Message Notifications using PHP and Pushbullet

I’ve been amusing myself following through an excellent series on self-hosting WordPress by Ashley Rich. Mostly so I can experiment with Nginx, pinch the best ideas and use them myself ๐Ÿ™‚

Everything had gone swimmingly on a test VPS running the latest LTS version of Ubuntu, but when I tried to implement some of the ideas on the shared hosting1 where this blog runs I ran into a bit of trouble. I was trying to get WordPress installation checksum verification2 and push messaging via Pushbullet working, but the https connect to the Pushbullet API would always fail. After some experimentation it turned out that the command line version of Curl was ancient3 and it wasn’t going to work. I already knew I was wasting my time asking the hosting company to upgrade the version; I had tried that before with another shell program that was obsolete and they (politely) declined4. So, time to get programming!

I already know that the python version on the hosting is ancient and doesn’t have SSL support at all5, ditto Perl, so I am going to have to use PHP. I won’t have a problem with that as there are versions up to 7.1 on this host and the SSL libraries are properly current too. Command-line PHP isn’t great, but it will work just fine for this application.

Here is the resulting simple program to send a notification using the Pushbullet API. This is called from a Bash script if the WordPress verification checks fail; the resulting warning message turns up on all my registered devices.

<?php
/**
* PHP program to send messages to pushbullet.
* Usage: php message-pushbullet.php creds='...' title='...' body='...' [quiet] [headers]
*/
date_default_timezone_set('UTC');
error_reporting (E_ALL);

$url = "https://api.pushbullet.com/v2/pushes";
$type = "note";

if(php_sapi_name() == 'cli'){
    //convert cli params to GET params (chop off name of program which is 1st)
    parse_str(implode('&', array_slice($argv, 1)), $_GET);
}
//process parameters
$credentials = isset($_GET['creds']) ? trim($_GET['creds']) : "";
$title = isset($_GET['title']) ? trim($_GET['title']) : "";
$body = isset($_GET['body']) ? trim($_GET['body']) : "";
$quiet = isset($_GET['quiet']) ? true : false; //silent mode
$headers = isset($_GET['headers']) ? true : false; //show headers for debugging

if(!$credentials || !$title || !$body){
    echo "Usage: message-pushbullet creds='...' title='...' body='...' [quiet] [headers]\n";
    exit(1);
}

$post_fields = json_encode(compact('type', 'title', 'body'));

$ch = curl_init();

$options = [
    CURLOPT_URL => $url,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_MAXREDIRS => 6,
    CURLOPT_HTTPHEADER => ['Access-Token: ' . $credentials, 
                            'Content-Type: application/json',
                            'Content-Length: ' . strlen($post_fields)],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $post_fields,
    CURLOPT_HEADER => $headers,
];
curl_setopt_array($ch, $options);

$result = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if(!$quiet){
    echo $result;
}
//check ok
if($status == 200){
    exit (0);
}
//failed
exit(1);

/* end */

Command-line parameter handling isn’t great as standard, so I cheat and turn them into GET parameters6. This handily means the program would work if used as a web script too with no further effort.

Oddly the API docs specify the parameters as JSON, but the version Ashley Rich created uses standard POST-style variables set using the CURL -d option and this works too. The Pushbullet API talks about maintaining backwards-compatibility to earlier versions, so I suspect there was a pre-JSON version at one time.

Most of the time was spent sorting out the Curl parameters needed ๐Ÿ™‚

 

Simple Python SMTP mail script – Part 1

TSOHostI host this blog via TSOHost in the UK. On the surface they look like just another low-cost hosting provider, but under the covers they are actually quite programmer-friendly. My cheapo hosting plan has SSH access, cron jobs, up-to-date PHP7 and Mysql and surprisingly good tech support. All good so far and way in excess of the average WordPress user’s needs.

However, I’m not an average (or sensible) WordPress user, so when I started actually writing blog posts I also started thinking about off-site backups and it was far too easy and boring just to install a backup plugin and call it done. As a minor part of my day job I am involved in using Amazon web services, mostly S3 for cloud storage so I started musing about sticking my blog backups in a S3 bucket.

I already had a bash script to create a db backup, tar the wp-content dir, gzip it all up and send it to S3, so stick it on a cron job and pretty much job done eh? … Not quite; I like to get a daily email reminder that the backup ran and some info about the result.

…and that is when things started to unravel…

Shared hosting is cheap, but it comes with quite a few limitations compared to non-shared alternatives7. The main problem is that shared hosting is a magnet for every spammer and scammer around, so the hosting providers have to severely limit access to normal utilities like command-line mail and sendmail. Even when these utilities are there and work, the outgoing mail host is probably on every blacklist around so your mail is going nowhere. This is true for TSOHost too; I couldn’t use any of the easy ways of sending mail from my backup script.

However, all is not lost, they provide an internal SMTP server inside their cloud server farm that can send mails and I already know that works fine as I have used it from one of my PHP applications. Problem solved?

Not quite. Another issue with shared hosting appears – a lot of expected utilities are missing from the command line (ssmtp? – nope) and what is there is old. Like – really old8. They can’t be updated and nothing can be installed except user-land programs.

So how about doing something is a scripting language? TSOHost runs the latest PHP and I could pull down PHPMailer and whip a script up pretty quickly; I’m pretty good with PHP, so that would be easy. It just feels messy though – I started with a single bash script and I’d end up bolting another script and external libraries on the side just to send an email.

How about Perl? Version 5.8.89 is installed and it has the Net::SMTP module. It doesn’t have the Net::SMTP::SSL module though, but that is ok as the TSOHost internal SMTP server doesn’t need encrypted connections. Yes?

No. I don’t really know Perl beyond doing superficial one-liners in shell scripts; plus there is something …painful about Perl10. I’ll consider that as a last resort.

How about Python? Version 2.4.311 is there, the smtplib module loads. Again, SSL won’t work as that didn’t come in until v2.6, but not a problem here. Yes?

Yes! I don’t know Python well either, but that’s what Skynet-Beta Google is for eh?

To be continued…

 

WordPress Plugin WP-Mail-SMTP Self-signed Certificate Patch

I am using an excellent plugin by Callum Macdonald to send mail from my WordPress blog. This works absolutely fine with the various mail-servers that I have pointed it at, but embarrassingly not with my own home mail-server12.

Now, this isn’t exactly a problem since I don’t need to send mail via my own home mail-server from my blog, but as with all things geeky I got curious why it wouldn’t work. I know the mail-server works fine13 and I can connect and send /receive from all my computers and devices, so I was a bit puzzled. What was especially odd is that it worked from WAMP on my laptop, but not from a test site in a Vagrant virtual machine on the same laptop. So I went Googling and found this. Doh! My WAMP installation is still on PHP 5.514, but the Vagrant instance is on 7.1 – and I still have a self-signed certificate on my mail-server.

So; just because I could15, I created a very simple WordPress plugin to apply a filter to WP Mail SMTP to add the extra parameters for self-signed certificate connections. The code looks like this:

add_filter( 'wp_mail_smtp_custom_options' , function( $phpmailer ){

    $phpmailer->SMTPOptions = array(
        'ssl' => array(
            'verify_peer' => false,
            'verify_peer_name' => false,
            'allow_self_signed' => true
        )
    );
    return $phpmailer;
});

Couldn’t be simpler eh? This can be stuck in functions.php or in a plugin like I did16.

My plugin can be downloaded here: WP-Mail-SMTP Self-signed Certificate Patch Plugin.

Now; just a work of caution – you really shouldn’t do this. You should replace your self-signed certificate with a real one from Let’s Encrypt or a commercial provider. There’s not much excuse for using self-signed certificates now that it is so easy and cheap / free to get a real one (except for testing).

I had even less excuse as I already have a Let’s Encrypt certificate on Apache running on the same physical server as the mail-server17. After making and testing this plugin I actually fixed the certificate problem properly and I can confirm that exactly the same certificates that work for Apache (and Nginx) also work with Postfix. The only remaining issue that I need to sort is to modify the routine that re-starts Apache on certificate renewal to also restart Postfix.

A few minutes work and I’ve got until June to do it ๐Ÿ˜‰

 

 

Prevent Non-Authenticated Access to WordPress REST API

I only recently became aware of the shenanigans from February with hackers attacking vulnerabilities in the WordPress Json REST API to deface sites. I wasn’t affected by this; and in actuality I hadn’t taken notice of the Api at all since I haven’t done anything yet that needs it.

Now I have taken notice and IMO there is a fundamental issue with it as it allows non-authenticated access to quite a lot of blog data in a way that is easy to probe very quickly with automated hacking tools18. ย Now this is kinda- not a big deal as the accessible data is supposedly public anyway, but after having a quick poke around the api end points I found a glaring security hole that I really don’t want to expose.

Navigating to the “wp-json/wp/v2/users” endpoint lists users that also authored pages, not just the users that have visible published posts. That includes my admin user that created the “About” and “Contact” pages and I really don’t want to expose any clues about the site administrator19.

So, I’m not happy at all having the api exposed, but fortunately the fix is very easy and straight out of the documentation. Adding the following code20 to the functions.php or a site-specific plugin restricts all api access to authenticated users:

add_filter( 'rest_authentication_errors', function( $result ) {
    if ( ! empty( $result ) ) {
        return $result;
    }
    if ( ! is_user_logged_in() ) {
        return new WP_Error( 'rest_not_logged_in', 'You are not currently logged in.', 
        array( 'status' => 401 ) );
    }
    return $result;
});

This works fine and keeps the api function intact, unlike other solutions that block the “users” endpoint completely21.

Another interesting thing I found is that requesting a list of categories in the api using “wp-json/wp/v2/categories” lists all categories, not just ones used in published posts – thereby revealing non-publicly available data – FAIL! Who knows what other holes are there in the api22???

Anyone else got misgivings about the potential security of the WordPress REST API?

[PS: For people not comfortable with coding there is a plugin that I believe uses the same approach as I do: Disable REST API by Dave McHale]

 

How to Make the WordPress Post Editor Default to Opening Links in a New Tab

This one definitely fits into the “Minor annoyances have a ridiculous amount of effort expended on them” category. See the pic for the minor annoyance; I had to click the “open link in a new tab” checkbox every time I added an external link in a post. One second wasted? Two tops on a slow day pre-coffee.

I should have just shrugged my shoulders and moved on, but that is not how things work in geek-land ๐Ÿ˜‰

So; I went searching. I was a bit surprised to discover that there wasn’t a way of setting this default, it seemed obvious. Maybe there was a plugin? Nope23.

Maybe someone else had asked the question? Yes they had and there was a solution … which was wrong as it didn’t work24.

OK, I can see where this is going. I’m quite good at Javascript, so how hard can it be?25

Turns out the solution is harder than you may think and it requires supplying a setup function for TinyMCE26. I found a post on a related topic here which also doesn’t work (it’s for an older version of TinyMCE), but gave me enough clues that I could put the rest of the pieces together myself after reading the V4 docs.

The code snippet below is the solution; I stuck it in a site-specific plugin I already had, but it could go in functions.php or wherever:

add_filter('tiny_mce_before_init', function($initArray){
    //add a setup fn
    $initArray['setup'] = <<<JS
function(editor) {
    //catch the ExecCommand event. The tinyMce mceInsertLink command then
    //triggers a custom WP_Link command
    editor.on('ExecCommand', function (e) {
      //console.debug('Command: ', e);
      if(e.command === 'WP_Link'){
          //release to UI so link value is populated
          setTimeout(function(){
            var linkSel = document.querySelector('.wp-link-input input');
            
            if(linkSel.value === ''){
                //no link so set the "Open link in a new tab" checkbox
                //to force the default as checked
                var linkTargetCheckSel = document.querySelector('#wp-link-target');
                linkTargetCheckSel.checked = true;
            }
          }, 0);
      };
    });

}
JS;
    $initArray['setup'] = trim($initArray['setup']); //prevent leading whitespace before fn
    return $initArray;
});

The trick is to catch the WP_Link event that the editor emits, look at the link value to see if it is blank and if it is, set the “open link in new tab” checkbox so that becomes the default.

Works just fine and would make the lamest plugin ever ๐Ÿ™‚

[Update Dec 17 – Bah! one of the WP updates since I created this solution has broken it. I’ll research what happened and try and fix it again when I can be bothered. ]

Fun with Nginx: Getting WordPress to work in a Sub-Directory

Well that was fun. After a marathon google session and an almost infinite number of reloads of Nginx I got wordpress with pretty permalinks working in an aliased sub-directory on my windows test laptop.

(Clue – almost every post explaining how to do this is incorrect. An exercise for the interested reader is to find the one that worked …)

This is the simplified excerpt from a server{} directive. [The wordpress root is mapped from somewhere else into the /wordpress sub-directory using the alias directive.]

location @wp {
  rewrite ^/wordpress(.*) /wordpress/index.php?$1;
}

location /wordpress {
    alias "c:/webserver/www/websites/wpnm"; #where wp actually is
    try_files $uri $uri/ @wp;

    #other config stuff here....
    
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_read_timeout 300s;
        fastcgi_pass 127.0.0.1:9123;
        fastcgi_param  SCRIPT_FILENAME $request_filename;

        include fastcgi_params;
    }
}

The key bit is the try_files directive looking for a real physical file and then when it isn’t found the @wp rewrite directive grabs the pretty permalink and stuffs it into a query string tacked on the index.php. This works with all the permalink variations that wp offers.

If WordPress is simply running in the root directory then this is easy-peasy and the re-write isn’t needed – simply stick /index.php?$args as the last term of try_files instead of @wp. That config can easily be found on line

The best resource I found for getting to grips with Nginx’s config was a tutorial on Linode’s website: https://www.linode.com/docs/websites/nginx/how-to-configure-nginx. This explains how the order of processing of the location directives works; essential knowledge to do anything above a very basic level.

Creating these configs isn’t that easy due to the limited debug tools. It’s not easy to see why things don’t work and the logs don’t help.

The hilariously uninformative error messages from the fastcgi module are typical – almost any config mistake produces a blank page with “No input file specified.”. Niiiice ๐Ÿ™‚ ย Good luck finding out just what the incorrect parameter was ๐Ÿ˜‰

The best debug suggestions I have found are in this blog post: https://blog.martinfjordvald.com/2011/01/no-input-file-specified-with-php-and-nginx/. I had a bit of fun since I started withย $document_root$fastcgi_script_name as the SCRIPT_FILENAME as the Nginx docs suggest, but that doesn’t work when using aliases.

Of course, that little gem is not well documented…..