Going wild with WordPress notifications

It is a nice security feature to receive a notification on my phone when someone logs in to my WordPress website

You want to receive a notification on you Android device, when someone logs in on your WordPress site? Perhaps as a security measurement?

In this post we will build a system that allows you to do so. In the first part of the post I will guide you though the WordPress side of this and in the second part I will show you how to create a very basic Android app that receives the notifications. At the end we will have two separate projects:

  • A firebase-actions plugin for WordPress
  • A Firebase Actions Android app that can receive the notifications

Note that in case you are more interested in developing an iOS app this shouldn’t be a problem, the steps for iOS should be almost the same as those for Android.

The application will be secured by using a server token to communicate with Firebase and on the Android side, only apps signed with your certificate will be able to register for cloud messages.

All code for this post is available on github:

  1. WordPress plugin: https://github.com/nickmartens/firebase-actions-wordpress.
  2. Android App: https://github.com/nickmartens/firebase-actions-android

Part 1: Creating a WordPress plugin

The first step is creating a WordPress plugin that can receive the events as they happen. As you may know, WordPress is written is PHP. Unfortunately PHP is not my most fluent languages, but sending a simple HTTP message to Firebase should not be too much of a problem. So, let’s get started!

Connecting to Firebase

First things first, to connect to firebase we first need to set up Firebase cloud messaging. Which is not that hard. The first step is to create a Firebase account at firebase.google.com. Next, you need to go to the Firebase console and get your server key and sender id. We will need these later when the plugin is ready to use.

Creating the WordPress plugin

To create a new plugin we simply need to create a new folder in WordPress’ plugins folder. I named mine firebase-actions. The one thing we need in there is a firebase-actions.php file that acts as the entry point of the plugin and is the file WordPress will load. This php file has the same name as the folder containing it.

The next step is to connect to interesting Wordpress hooks and send events to Firebase as these events happen. For the first implementation I choose five hooks that could be interesting. These hooks are login, authenticate, save_post, publish_post and publish page. So in that file, add the following code:

add_action( 'wp_login', __NAMESPACE__ . '\\fa_init_wp_login', 10, 2 );
add_action( 'wp_authenticate', __NAMESPACE__ . '\\fa_init_wp_authenticate' );
add_action( 'save_post', __NAMESPACE__ . '\\fa_init_save_post' );
add_action( 'publish_post', __NAMESPACE__ . '\\fa_init_publish_post', 10, 2 );
add_action( 'publish_page', __NAMESPACE__ . '\\fa_init_publish_page', 10, 2 );

To clarify, The first line connects the ‘wp_login‘ event, to the fa_init_wp_login function. So whenever there is a login event, the fa_init_wp_login function will be called. 10 is the priority of this connection and 2 tells WordPress that we would like to receive two parameters from the login call.

The implementation of the callback looks like this:

function fa_init_wp_login( $user_login, $user ) {
   _do_post( "login", 'User: ' . $user_login . ' logged in', $user_login, null );
}

So the only thing it really does is forward the event to the _do_post method. This method does the actual work:

function _do_post( $refPath, $title, $message, $url ) {
    $options = get_option( 'fa_options' );
    $server_key = $options['server_key'];

    $data = [
        'title' => print_r( $title, true ),
        'body' => print_r( $message, true ),
        'url' => print_r( $url, true ),
        'request_time' => print_r( $_SERVER['REQUEST_TIME'], true ),
        'remote_addr' => print_r( getenv( 'REMOTE_ADDR' ), true ),
        'forwarded_for' => print_r( getenv( 'HTTP_FORWARDED_FOR' ), true )
    ];

    $topic = '/topics/' . $refPath;

    $body = [
        'data' => $data,
        'to' => $topic
    ];

    $headers = array
    (
        'Authorization: key=' . $server_key,
        'Content-Type: application/json'
    );

    $ch = curl_init();
    curl_setopt( $ch, CURLOPT_URL, '//fcm.googleapis.com/fcm/send' );
    curl_setopt( $ch, CURLOPT_POST, true );
    curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
    curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
    curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
    curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( $body ) );
    $result = curl_exec( $ch );
    curl_close( $ch );
    error_log( "fcm result: " . $result );
}

As you can see, this method connects to Firebase, using the server key. Next it sends the message and it adds a few more details about the request. Perhaps the most interesting part is the way the data is composed before it is sent to the server. Firebase requires your data message to be strings only. For that reason I print_r($var, true) all of the values. This ensures every value is sent as a string. That is all we need to be able to send an event to Firebase.

As you can see, the $server_key variable get initialized from the options. But for this to work, we actually need to create an options page so we can save the configuration options within the WordPress system.

Creating a configuration page

A configuration page makes it easier to dynamically configure the plugin. Most of the details about configuration pages are out of scope for this post. You can find everything related to doing this in the tutorial to create option pages at the WordPress Codex. I will highlight the interesting bits.

To get your admin page in the admin section, you need to register two callbacks. Like this:

add_action( 'admin_menu', array( $this, 'add_plugin_page' ) );
add_action( 'admin_init', array( $this, 'page_init' ) );

This is again just a simple add_action call, just like registering the hooks for the interesting events. The next step is configuring the settings page. The full implementation looks like this:

class FirebaseSettingsPage {
	/**
	 * Holds the values to be used in the fields callbacks
	 */
	private $options;

	/**
	 * Start up
	 */
	public function __construct() {
		add_action( 'admin_menu', array( $this, 'add_plugin_page' ) );
		add_action( 'admin_init', array( $this, 'page_init' ) );
	}

	/**
	 * Add options page
	 */
	public function add_plugin_page() {
		// This page will be under "Settings"
		add_options_page(
			'Firebase Actions Admin',
			'Firebase Actions',
			'manage_options',
			'my-setting-admin',
			array( $this, 'create_admin_page' )
		);
	}

	/**
	 * Options page callback
	 */
	public function create_admin_page() {
		// Set class property
		$this->options = get_option( 'fa_options' );
		?>
<div class="wrap">
<h1>Firebase Actions</h1>
<form method="post" action="options.php">
				<?php // This prints out all hidden setting fields 
                settings_fields( 'my_option_group' ); 
                do_settings_sections( 'my-setting-admin' ); 
                submit_button(); ?>
            </form>
        </div>
		<?php
	}

	/**
	 * Register and add settings
	 */
	public function page_init() {
		register_setting(
			'my_option_group', // Option group
			'fa_options', // Option name
			array( $this, 'sanitize' ) // Sanitize
		);

		add_settings_section(
			'setting_section_id', // ID
			'Firebase Actions Settings', // Title
			array( $this, 'print_section_info' ), // Callback
			'my-setting-admin' // Page
		);

		add_settings_field(
			'server_key',
			'Server key',
			array( $this, 'server_key_callback' ),
			'my-setting-admin',
			'setting_section_id'
		);

		add_settings_field(
			'sender_id', // ID
			'Sender id', // Title
			array( $this, 'sender_id_callback' ), // Callback
			'my-setting-admin', // Page
			'setting_section_id' // Section
		);
	}

	/**
	 * Sanitize each setting field as needed
	 *
	 * @param array $input Contains all settings fields as array keys
	 */
	public function sanitize( $input ) {
		$new_input = array();
		if ( isset( $input['sender_id'] ) ) {
			$new_input['sender_id'] = sanitize_text_field( $input['sender_id'] );
		}

		if ( isset( $input['server_key'] ) ) {
			$new_input['server_key'] = sanitize_text_field( $input['server_key'] );
		}

		return $new_input;
	}

	/**
	 * Print the Section text
	 */
	public function print_section_info() {
		print 'Configure your settings below:';
		$options = get_option( 'fa_options' );

		if ( ! $options ) {
			print '
<b>Warning:</b> No configuration found. You need to set the server key and sender id first';
			return;
		}

		$server_key = $options['server_key'];
		$sender_id  = $options['sender_id'];

		if ( ! $server_key || ! $sender_id ) {
			print '
<b>Warning:</b> No configuration found. You need to set the server key and sender id first';
			return;
		}
	}

	/**
	 * Get the settings option array and print one of its values
	 */
	public function sender_id_callback() {
		printf(
			'<input type="text" id="sender_id" name="fa_options[sender_id]" value="%s" />',
			isset( $this->options['sender_id'] ) ? esc_attr( $this->options['sender_id'] ) : ''
		);
	}

	/**
	 * Get the settings option array and print one of its values
	 */
	public function server_key_callback() {
		printf(
			'<textarea id="server_key" name="fa_options[server_key]">%s</textarea>',
			isset( $this->options['server_key'] ) ? esc_attr( $this->options['server_key'] ) : ''
		);
	}
}

The interesting bits are: the page_init method and the sanitize method. The first method adds the additional settings fields to the screen. The second method cleans up the user input and returns the cleaned up array, which is saved by WordPress.

The generated page then looks like this:

Configuration page

That is all for the WordPress plugin!

Part 2: The Android App

Now that we have a plugin that can send the data to the Firebase server, the next step it to create a simple Android App that receives the notifications from Firebase.

Connect to Firebase

The first part is to connect the App to Firebase. You can do this from the tools -> Firebase menu in Android Studio. Just open the cloud messaging part and follow the instructions on the screen.

The setup screen should look like this:

Firebase assistent

Once that is done, and the dependencies are set we can add the code to receive the messages. In case you run into problems linking the App, you need to download the google-services.json from the Firebase console and replace the one in your Android project.

Registering for notifications

Update the generated MainActivity with the code to register to the topics.

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        FirebaseMessaging.getInstance().subscribeToTopic("log");
        FirebaseMessaging.getInstance().subscribeToTopic("login");
        FirebaseMessaging.getInstance().subscribeToTopic("auth");
        FirebaseMessaging.getInstance().subscribeToTopic("save_post");
        FirebaseMessaging.getInstance().subscribeToTopic("new_article");
        FirebaseMessaging.getInstance().subscribeToTopic("new_post");
    }
}

This is all that is needed to register to Firebase. The topics are the same topics we used in the WordPress plugin:

    _do_post( "login", 'User: ' . $user_login . ' logged in', $user_login, null );

That first parameter in the _do_post call, is the topic to post to.

Handling notifications

Notifications are received by a FirebaseMessagingService. If you followed the instructions when you connected to Firebase in Android Studio, you should have created such a class.

My implementation looks like this:

public class MessageService extends FirebaseMessagingService {

    private static final String TAG = "MessageService";

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        super.onMessageReceived(remoteMessage);

        String from = remoteMessage.getFrom();

        // Check if message contains a data payload.
        Map<String, String> data = remoteMessage.getData();
        if (data.size() > 0) {
            Log.d(TAG, "Message data payload: " + data);
            switch (from) {
                case "/topics/log":
                    onLogMessage(data);
                    break;
                case "/topics/login":
                ...
                ...
                case "/topics/new_page":
                    onNewPageMessage(data);
                    break;
            }
        }

        // Check if message contains a notification payload.
        if (remoteMessage.getNotification() != null) {
            Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
        }

    }

    private void onNewPageMessage(Map<String, String> data) {
        String title = data.get("title");
        if (!TextUtils.isEmpty(title)) {
            String body = data.get("body");
            Notification notification = new NotificationCompat.Builder(this)
                    .setSmallIcon(R.drawable.ic_stat_default_24dp)
                    .setColor(0xFFFFD146)
                    .setContentTitle(title)
                    .setContentText(body)
                    .setAutoCancel(true)
                    .setDefaults(DEFAULT_ALL)
                    .setGroup("Warmbeer blog")
                    .setStyle(new NotificationCompat.BigTextStyle()
                            .bigText(body)
                            .setBigContentTitle(title)
                            .setSummaryText(body))
                    .build();
            showNotification(5, notification);
        }
    }

    ...
    // More methods handling the other topics

    private void showNotification(int id, Notification notification) {
        NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        nm.notify(id, notification);
        Notification group = new NotificationCompat.Builder(this)
                .setSmallIcon(R.drawable.ic_stat_default_24dp)
                .setColor(0xFFFFD146)
                .setGroup("Warmbeer blog")
                .setContentTitle("")
                .setGroupSummary(true)
                .build();
        nm.notify(12, group);
    }
}

When a message is received is the onMessageReceived method will be called for you. The first thing we do there, is getting a hold of the topic this message was sent to. With the topic we can determine what data the message contains and extract that data to create a notification.

The code to generate the new_page notification in the WordPress plugin is as follows:

function fa_init_publish_page( $ID, $post )
{
    $author = $post->post_author; /* Post author ID. */
    $name = get_the_author_meta( 'display_name', $author );
    $title = $post->post_title;
    $permalink = get_permalink( $ID );
    $subject = sprintf( 'New page published: %s', $title );
    $message = sprintf( '%s published a new page: “%s”.', $name, $title );
    $message .= sprintf( 'View: %s', $permalink );
    _do_post( 'new_page', $subject, $message, $permalink );
}

As you can see, the more data we add to the notification, the more we can use in the app. After processing the data, the notification looks like this:

What’s next?

Now that we can post notifications from WordPress, it may also be useful to post information from the server itself. Like for example the output of certain cron jobs.

For example, let’s say we have an SSL certificate from let’s encrypt on the server, that we try to update every week. It would be useful to know the output of the command. From a weekly cron job we can call a script like this:

#!/bin/sh
/usr/bin/letsencrypt renew | tee -a /var/log/le_renew.log | post-out-to-firebase.sh

As you can see the output is sent to another script that deals with posting the output to the Firebase server. This script looks like this:

#!/bin/sh
output=$(tee)

jq -n --arg message "$output" \
      --arg topic "ssl" \
   '{to: "/topics/log", data: { topic: $topic, message: $message}}'|
curl -H "Content-Type: application/json" \
   -H 'Authorization: key=SERVER_SECRET_HERE' \
   -X POST \
   -d@- \
   //fcm.googleapis.com/fcm/send

By using this setup my WordPress server sends me the output of the certificate renewal call every time it tries to renew. This is a very useful way to track the status of my certificate. This could be used for tons of other things as well. For example checking if a reboot is required because of an update, or to send me the output of certain other cron-jobs.

As you can see it was very easy to connect WordPress events to an app, and creating a simple WordPress plugin is really simple as well. Right now I created an Android app, but as noted before, creating an iOS app with this would also be quite easy. Firebase has support for iOS, so I can imagine it would also not be too difficult to create an iOS app for this. I am really interested to hear what else you can build with this!

Cheers, Nick!