Browser Push Notifications in Javascript and PHP

Foreword

In an attempt to find a good article on setting up notifications in a browser, I only received articles that mainly described the use in conjunction with Firebase, but this option was not particularly suitable for me.





In this article, the principles of operation and subtleties of Push notifications will not be "muddied", only the code, only the hardcore.





Important notes





Push notifications only work with HTTPS .

By the way, in addition to HTTPS, a valid SSL certificate must be present, Let's Encrypt will do.





Localhost is fine for development. There shouldn't be any problems, but if you do arise, this article will help you deal with them.





Let there be code

Authorization (VAPID)

First, it's worth installing the WebPush library into your php project:





$ composer require minishlink/web-push
      
      



Next, to authorize your server with a browser (VAPID), you need to generate public and private ssh keys. These keys will be needed both on the server and on the client (except that only the public one is needed on the client) .





To generate an uncompressed Base64 encoded public and private key, enter the following into your Linux bash:





$ openssl ecparam -genkey -name prime256v1 -out private_key.pem
$ openssl ec -in private_key.pem -pubout -outform DER|tail -c 65|base64|tr -d '=' |tr '/+' '_-' >> public_key.txt
$ openssl ec -in private_key.pem -outform DER|tail -c +8|head -c 32|base64|tr -d '=' |tr '/+' '_-' >> private_key.txt
      
      



The author of the library also provides the generation of vapid keys using the built-in method:





$vapidKeysInBase64 = VAPID::createVapidKeys();
      
      



Subscription

Stage 1 (JS)

ServiceWorker, PushManager, showNotification :





app.js





function checkNotificationSupported() {
	return new Promise((fulfilled, reject) => {
  	if (!('serviceWorker' in navigator)) {
      reject(new Error('Service workers are not supported by this browser'));
      return;
    }

    if (!('PushManager' in window)) {
      reject(new Error('Push notifications are not supported by this browser'));
      return;
    }

    if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
      reject(new Error('Notifications are not supported by this browser'));
    	return;
    }
    
    fulfilled();
  })
}
      
      



sw.js :





app.js





navigator.serviceWorker.register('sw.js').then(() => {
      console.log('[SW] Service worker has been registered');
    }, e => {
      console.error('[SW] Service worker registration failed', e);
    }
  );
      
      



:





app.js





function checkNotificationPermission() {
    return new Promise((fulfilled, reject) => {
        if (Notification.permission === 'denied') {
            return reject(new Error('Push messages are blocked.'));
        }
        if (Notification.permission === 'granted') {
            return fulfilled();
        }
        if (Notification.permission === 'default') {
            return Notification.requestPermission().then(result => {
                if (result !== 'granted') {
                    reject(new Error('Bad permission result'));
                } else {
                    fulfilled();
                }
            });
        }
        return reject(new Error('Unknown permission'));
    });
}
      
      



ssh :





<script>
	window.applicationServerKey = '<?= $yourPublicKeyFromServer ?>'
</script>
      
      



, . 10 .





app.js





document.addEventListener('DOMContentLoaded', documentLoadHandler);


function documentLoadHandler() {
    checkNotificationSupported()
        .then(() => {
          setTimeout(() => {
            serviceWorkerRegistration.pushManager.subscribe({
                    userVisibleOnly: true,
                    applicationServerKey: urlBase64ToUint8Array(window.applicationServerKey),
                })
                .then(successSubscriptionHandler, errorSubscriptionHandler)
          }, 10000);
         }, 
        	console.error
      	);
}


function urlBase64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
    const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);
    for (let i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
}

function errorSubscriptionHandler(err) {
    if (Notification.permission === 'denied') {
        console.warn('Notifications are denied by the user.');
    } else {
        console.error('Impossible to subscribe to push notifications', err);
    }
}
      
      



successSubscriptionHandler





.





app.js





function successSubscriptionHandler(subscriptionData) {
    const key = subscription.getKey('p256dh');
    const token = subscription.getKey('auth');
    const contentEncoding = (PushManager.supportedContentEncodings || ['aesgcm'])[0];
    const body = new FormData();

    body.set('endpoint', subscription.endpoint);
    body.set('publicKey', key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : null);
    body.set('authToken', token ? btoa(String.fromCharCode.apply(null, new Uint8Array(token))) : null);
    body.set('contentEncoding', contentEncoding);

    return fetch('src/push_subscription.php', {
      method,
      body,
    }).then(() => subscription);
  }
      
      







Post Message API





self.addEventListener('push', function (event) {
    if (!(self.Notification && self.Notification.permission === 'granted')) {
        return;
    }

    const sendNotification = body => {
        const title = " ";

        return self.registration.showNotification(title, {
            body,
        });
    };

    if (event.data) {
        const message = event.data.text();
        event.waitUntil(sendNotification(message));
    }
});
      
      



2 (PHP)

php 7+





subscribeUserToPushNotifications ,





subscribeUserToPushNotifications.php





<?php 

$subscription = $_POST;
if (!isset($subscription['endpoint'])) {
    echo 'Error: not a subscription';
    return;
}

// save subscription from => $subscription 
      
      



( ), .









, :





pushNotificationToClient.php





<?php 

use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;

$subscription = Subscription::create($subscriptionData);
      
      



VAPID :





pushNotificationToClient.php





<?php 

$auth = array(
    'VAPID' => array(
        'subject' => 'https://your-project-domain.com',
        'publicKey' => file_get_contents(__DIR__ . '/your_project/keys/public_key.txt'),
        'privateKey' => file_get_contents(__DIR__ . '/your_project/keys/private_key.txt'), 
    )
);
      
      



, WebPush:





pushNotificationToClient.php





<?php

$webPush = new WebPush($auth);
      
      



! Push





<?php

$report = $webPush->sendOneNotification(
  $subscription,
  "  ,     sw.js"
);
      
      



Important note





To send notifications in iteration, you should use a function with the same parameters as in the function above:





$webPush->queueNotification







Helpful Sources

  1. About push technology





  2. About WebPush from Khabrovchanin





  3. WebPush library





  4. An example of use from a library developer








All Articles