Current File : /home/pacjaorg/www/kmm/media/com_akeebabackup/js/WebPush.js |
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
/**
* Akeeba Web Push integration.
*
* Window events being dispatched:
*
* * onAkeebaBackupWebPushNotSubscribed when the user is not subscribed to push notifications or just unsubscribed.
* * onAkeebaBackupWebPushSubscribed when the user is already subscribed to push notifications or just subscribed.
*/
((document, window, navigator) => {
class AkeebaBackupWebPushIntegration
{
options = {};
/**
* Public constructor
*
* @param {Object} options The configuration options
*/
constructor(options)
{
this.options = options;
}
/**
* Convert a Base64Url-encoded string to a byte array
*
* @param {string} base64String The string to convert
* @returns {Uint8Array} The resulting array
* @since 9.3.1
* @internal
*/
#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;
}
/**
* Disable the Web Push subscription interface.
*
* Used when the browser does not support Service Workers or the Web Push Manager object.
*
* @since 9.3.1
*/
disableInterface()
{
document.querySelector(this.options.subscribeButton).classList.add('d-none', 'disabled');
document.querySelector(this.options.unsubscribeButton).classList.add('d-none', 'disabled');
document.querySelector(this.options.unavailableInfo).classList.remove('d-none');
document.getElementById('webPushDetails').classList.add('d-none');
}
/**
* Registers a Service Worker to listen to our Web Push messages.
*
* @returns {null|Promise<ServiceWorkerRegistration>}
* @since 9.3.1
*/
registerServiceWorker()
{
console.debug('[Web Push] Preparing to register a Service Worker.');
const workerUri = this.options.workerUri;
if (!workerUri)
{
console.error('[Web Push] The backend provided no Service Worker URI. Cannot proceed.');
return null;
}
return navigator.serviceWorker
.register(workerUri)
.then((registration) => {
console.log('[Web Push] Service worker successfully registered.');
return registration;
})
.catch((err) => {
console.error('[Web Push] Unable to register Service Worker.', err);
return null;
})
}
/**
* Ask for permission to send Web Push notifications
*
* @returns {Promise<unknown>}
* @since 9.3.1
*/
askPermission()
{
return new Promise((resolve, reject) => {
console.debug('[Web Push] Requesting permission.');
const permissionResult = Notification
.requestPermission((result) => {
resolve(result);
});
if (permissionResult)
{
permissionResult.then(resolve, reject);
}
}).then((permissionResult) => {
if (permissionResult !== 'granted')
{
console.error('[Web Push] Permission for push notifications was NOT granted.');
throw new Error("We were not granted permission.");
}
console.log('[Web Push] Permission for push notifications was granted.');
return true;
});
}
/**
* Gets the user Web Push registration and returns the subscription record.
*
* @returns {Promise<PushSubscription>|null}
* @since 9.3.1
*/
subscribeUserToPush()
{
console.debug('[Web Push] Preparing to subscribe the user.');
const workerUri = this.options.workerUri;
const publicKey = this.options.vapidKeys?.publicKey;
if (!workerUri)
{
console.error('[Web Push] The backend provided no Service Worker URI. Cannot proceed.');
return null;
}
if (!publicKey)
{
console.error('[Web Push] The backend provided no VAPID public Key. Cannot proceed.')
}
return navigator.serviceWorker
.register(workerUri)
.then((registration) => {
const subscriberOptions = {
userVisibleOnly: true,
applicationServerKey: this.#urlBase64ToUint8Array(publicKey)
};
return registration.pushManager.subscribe(subscriberOptions);
})
.then((pushSubscription) => {
console.log('[Web Push] Received PushSubscription');
console.debug(JSON.stringify(pushSubscription));
return pushSubscription;
});
}
/**
* Sends the user's Web Push subscription record to the server
*
* @param pushSubscription
* @returns {null|Promise<any>}
* @since 9.3.1
*/
saveUserSubscription(pushSubscription)
{
console.log('[Web Push] About to send the user subscription information to the backend.');
const subscribeUri = this.options.subscribeUri;
if (!subscribeUri)
{
console.error('[Web Push] The backend provided no subscription registration URL. Cannot proceed.');
return null;
}
const body = new FormData();
body.append('subscription', JSON.stringify(pushSubscription));
return fetch(subscribeUri, {
method: 'POST', headers: {
'X-CSRF-Token': Joomla.getOptions('csrf.token')
}, body: body
})
.then((response) => {
if (!response.ok)
{
console.error(`[Web Push] Server returned HTTP error ${response.status}. Cannot proceed.`);
throw new Error(`Server returned HTTP error ${response.status}.`);
}
return response.json();
})
.then((responseData) => {
if (!responseData || !responseData.success)
{
console.error(`[Web Push] Server returned invalid data: ${responseData}. Cannot proceed.`);
const additionalMessage = responseData?.error ?? '';
throw new Error('Bad response from server. ' + additionalMessage);
}
});
}
isUserSubscribed(serviceWorkerRegistration)
{
serviceWorkerRegistration.pushManager.getSubscription()
.then((subscription) => {
let myEvent;
if (!subscription)
{
myEvent = new CustomEvent('onAkeebaBackupWebPushNotSubscribed');
}
else
{
myEvent = new CustomEvent('onAkeebaBackupWebPushSubscribed', {
detail: {
subscription
}
});
}
window.dispatchEvent(myEvent);
})
.catch((err) => {
if (navigator.appVersion.includes('Safari'))
{
console.error(
'[Web Push] Safari does not yet support Web Push (even if it is enabled as an experimental feature)');
this.disableInterface();
return;
}
console.error('[Web Push] Cannot get push subscription status', err);
});
}
onSubscribeClick(e)
{
e.target.classList.add('disabled');
var theSubscription;
this.askPermission()
.then((junk) => {
return this.subscribeUserToPush();
})
.then((pushSubscription) => {
if (pushSubscription === null)
{
return null;
}
theSubscription = pushSubscription;
return this.saveUserSubscription(pushSubscription);
})
.then(() => {
e.target.classList.remove('disabled');
const myEvent = new CustomEvent('onAkeebaBackupWebPushSubscribed', {
detail: {
subscription: theSubscription
}
});
window.dispatchEvent(myEvent);
})
.catch((err) => {
e.target.classList.remove('disabled');
Joomla.renderMessages({error: [err.message]});
});
}
onUnsubscribeClick(e)
{
const that = this;
const elButton = e.target;
function removeFromServer(subscription)
{
console.log('[Web Push] About to send the user unsubscription information to the backend.');
const unsubscribeUri = that.options.unsubscribeUri;
if (!unsubscribeUri)
{
console.log(that.options);
console.error(
'[Web Push] The backend provided no unsubscription registration URL. Cannot proceed.');
return null;
}
const json = JSON.stringify(subscription);
const body = new FormData();
body.append('subscription', json);
return fetch(unsubscribeUri, {
method: 'POST', headers: {
'X-CSRF-Token': Joomla.getOptions('csrf.token')
}, body: body
})
.then((response) => {
if (!response.ok)
{
console.error(`[Web Push] Server returned HTTP error ${response.status}. Cannot proceed.`);
throw new Error(`Server returned HTTP error ${response.status}.`);
}
return response.json();
})
.then((responseData) => {
if (!responseData || !responseData.success)
{
console.error(`[Web Push] Server returned invalid data: ${responseData}. Cannot proceed.`);
throw new Error('Bad response from server.');
}
elButton.classList.remove('disabled');
// Send custom event
const myEvent = new CustomEvent('onAkeebaBackupWebPushNotSubscribed', {
detail: {
subscription: subscription
}
});
window.dispatchEvent(myEvent);
})
.catch((err) => {
elButton.classList.remove('disabled');
Joomla.renderMessages({error: [err.message]});
});
}
this.registerServiceWorker().then((reg) => {
elButton.classList.add('disabled');
reg.pushManager.getSubscription().then((subscription) => {
subscription.unsubscribe().then((successful) => {
if (!successful)
{
elButton.classList.remove('disabled');
return subscription;
}
removeFromServer(subscription);
})
}).catch((err) => {
elButton.classList.remove('disabled');
Joomla.renderMessages({error: [err.message]});
})
})
}
/**
* Initialise the Web Push integration
*
* @since 9.3.1
*/
init()
{
console.debug('[Web Push] Initialising');
if (!this.options.subscribeButton)
{
console.warn(
'[Web Push] The backend provided no element to trigger the user subscription process. Abort.');
}
const elSubscribeButton = document.querySelector(this.options.subscribeButton);
if (!elSubscribeButton)
{
console.info('[Web Push] The element to trigger the user subscription process does not exist.')
return;
}
if (!('serviceWorker' in navigator) || !('PushManager' in window))
{
console.warn('[Web Push] The browser is incompatible with the Web Push standard.');
this.disableInterface();
return;
}
// What to do if the user is already subscribed
window.addEventListener('onAkeebaBackupWebPushSubscribed', (e) => {
console.debug('[Web Push] The user is already subscribed.');
const elUnsubscribeButton = document.querySelector(this.options.unsubscribeButton);
if (elSubscribeButton)
{
elSubscribeButton.classList.add('d-none', 'disabled');
}
if (elUnsubscribeButton)
{
elUnsubscribeButton.classList.remove('d-none', 'disabled');
elUnsubscribeButton.addEventListener('click', (e) => {
this.onUnsubscribeClick(e);
});
}
});
window.addEventListener('onAkeebaBackupWebPushNotSubscribed', (e) => {
console.debug('[Web Push] The user is not yet subscribed.');
const elUnsubscribeButton = document.querySelector(this.options.unsubscribeButton);
if (elUnsubscribeButton)
{
elUnsubscribeButton.classList.add('d-none', 'disabled');
}
if (elSubscribeButton)
{
elSubscribeButton.classList.remove('d-none', 'disabled');
elSubscribeButton.addEventListener('click', (e) => {
this.onSubscribeClick(e);
});
}
});
// Register the service worker, then check the user's subscription
this.registerServiceWorker()
.then((serviceWorkerRegistration) => {
this.isUserSubscribed(serviceWorkerRegistration);
});
return this;
}
}
if (Joomla.getOptions('akeeba.webPush') ?? {})
{
const akeebaWebPush = new AkeebaBackupWebPushIntegration(Joomla.getOptions('com_akeebabackup.webPush') ?? {});
akeebaWebPush.init();
}
})(document, window, navigator);