#!/usr/bin/perl
#===========================================================================
#  push.cgi
#
#  This is a perl CGI script to show the web push notifications
#  feature. 
#
#  Usage:
#    [USER END]
#    On a modern web browser:
#    Connect to https://whatever-site-and-path/push.cgi and allow push notifications
#
#    [APP END]
#    From a shell script:
#    Issue the command 'perl push.cgi cmd=send text="Hello world"'
#
#    You can also open push.cgi?cmd=send from a browser, but if it is the same
#    user end browser, you miss some of the magic
#
#  Notes:
#   This is just a minimal setup, only to show the app end part
#
#  Requirements:
#   This script should be run under https in order to comply to
#   the callback components policy of the browser's subscription
#   service
#
#   Copyright 2021 Erich Strelow
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
#============================================================================

use strict 'vars';
use warnings;

#You may change this according to your host. The script neede RW access
use constant APP_CONF => 'push.conf';
#This is the app wide authentication key
my $server_key = { public => 'BCAI00zPAbxEVU5w8D1kZXVs2Ro--FmpQNMOd0S0w1_5naTLZTGTYNqIt7d97c2mUDstAWOCXkNKecqgS4jARA8',
   private => 'M6xy5prDBhJNlOGnOkMekyAQnQSWKuJj1cD06SUQTow'};

use CGI;
use Config::IniFiles;
use JSON;
use HTTP::Request::Webpush;
use LWP::UserAgent;
use MIME::Base64 qw( encode_base64url decode_base64url);

my $req=new CGI;

my $cmd=$req->param('cmd') || $req->url_param('cmd');


#=======================================================================================
# Worker JS Script. This gets installed in the UA operating system in order to
# receive push messages
#=======================================================================================
my $worker= <<'EOJ';
// Register event listener for the 'push' event.
self.addEventListener('push', function(event) {
  // Retrieve the textual payload from event.data (a PushMessageData object).
  // Other formats are supported (ArrayBuffer, Blob, JSON), check out the documentation
  // on https://developer.mozilla.org/en-US/docs/Web/API/PushMessageData.
  const payload = event.data ? event.data.text() : 'no payload';

  // Keep the service worker alive until the notification is created.
  event.waitUntil(
    self.registration.showNotification('HTTP::Request::Webpush example', {
      body: payload,
    })
  );
});
EOJ


#=======================================================================================
# Subscription  HTML/JS Script. This is the end user HTML page that
# launches the subcription the first time
#=======================================================================================
sub renderpush {

   my $path=$req->url();
   my $cmd=$req->url(-relative => 1);
   my $worker = "$path?cmd=service-worker.js";
   my $subscribe= "$path?cmd=subscribe";

   print <<"EOH";
<div class='push'>
<a href="#" onclick='return subscribe()'>Activate push notifications</a>
<script type='text/javascript'>
function isSupported() {
  if (!('serviceWorker' in navigator)) {
    // Service Worker isn't supported on this browser, disable or hide UI.
    return false;
  }

  if (!('PushManager' in window)) {
    // Push isn't supported on this browser, disable or hide UI.
    return false;
  }

  return true;
}

// Web-Push
// Public base64 to Uint
function urlBase64ToUint8Array(base64String) {
    var padding = '='.repeat((4 - base64String.length % 4) % 4);
    var base64 = (base64String + padding)
        .replace(/\-/g, '+')
        .replace(/_/g, '/');

    var rawData = window.atob(base64);
    var outputArray = new Uint8Array(rawData.length);

    for (var i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
}

async function subscribe() {
  const result = await Notification.requestPermission();
  if (result == 'granted') {
    var r=await navigator.serviceWorker.register('$worker');
    var m=r.pushManager;
    const subscribeOptions = {
       userVisibleOnly: true,
       applicationServerKey: urlBase64ToUint8Array(
        '$server_key->{public}'
      )
    };

    var s= await m.subscribe(subscribeOptions); 
    var w=fetch('$subscribe', {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json'
       },
       body: JSON.stringify(s)
        });
    }

   return true;
}
</script>

EOH
}

sub subscribe($) {

   my $opt=shift();
   my $conf=Config::IniFiles->new(-file => APP_CONF, -nocase => 1);
   die "Configuration fail" unless ($conf);
   $conf->newval('subscription','data',$opt);
   $conf->RewriteConfig;
   my $success='{ "data": { "success": "true" } }';
   print $req->header(-type => 'application/json', -Content_length => length($success));


}

sub postpush($$) {

   my $session=shift();
   my $text=shift();

   my $conf=Config::IniFiles->new(-file => APP_CONF, -nocase => 1);
   die "Configuration fail" unless($conf);
   my $json=$conf->val($session,'data');
   my $keys=from_json($json);

   my $send=HTTP::Request::Webpush->new(subscription => $keys);
   $send->authbase64($server_key->{public}, $server_key->{private});
   $send->content($text);
   $send->subject('mailto:estrelow@cpan.org');
   $send->encode();
   $send->header('TTL' => '90');

   my $ua = LWP::UserAgent->new;
   my $response = $ua->request($send);
   print $req->header("text/plain");

   print "Message sent\n";
   print $response->code();
   print "\n";
   print $response->decoded_content;
   print $response->header('Location');
   print "\n";
   print $response->header('Link');

}


if ($cmd eq 'service-worker.js') {
   print $req->header(-type       => 'application/javascript', -Content_length => length($worker));
   print $worker;
} elsif ($cmd eq 'subscribe') {
   subscribe($req->param('POSTDATA'));
}  elsif ($cmd eq 'send') {
   my $text= $req->param('text') || 'Hello world';
   postpush('subscription',$text);

}   else {
   print $req->header('text/html');
   renderpush;
}