0003-pwa-add-url-views-and-javascript-for-webpush-25462.patch
combo/apps/pwa/static/js/webpush.js | ||
---|---|---|
1 |
/* globals window, navigator, Uint8Array, $, console, Notification */ |
|
2 |
"use strict"; |
|
3 |
// Webpush Application controller for Combo |
|
4 |
// Copyright (C) 2018 Entr'ouvert |
|
5 |
// |
|
6 |
// This program is free software: you can redistribute it and/or modify it |
|
7 |
// under the terms of the GNU General Public License as published |
|
8 |
// by the Free Software Foundation, either version 3 of the License, or |
|
9 |
// (at your option) any later version. |
|
10 |
// |
|
11 |
// This program is distributed in the hope that it will be useful, |
|
12 |
// but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 |
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 |
// GNU General Public License for more details. |
|
15 |
// |
|
16 |
// You should have received a copy of the GNU General Public License |
|
17 |
// along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
18 | ||
19 |
// Utils functions: |
|
20 |
function loadVersionBrowser(userAgent) { |
|
21 |
var ua = userAgent, |
|
22 |
tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; |
|
23 |
if (/trident/i.test(M[1])) { |
|
24 |
tem = /\brv[ :]+(\d+)/g.exec(ua) || []; |
|
25 |
return { |
|
26 |
name: 'IE', |
|
27 |
version: (tem[1] || '') |
|
28 |
}; |
|
29 |
} |
|
30 |
if (M[1] === 'Chrome') { |
|
31 |
tem = ua.match(/\bOPR\/(\d+)/); |
|
32 |
if (tem != null) { |
|
33 |
return { |
|
34 |
name: 'Opera', |
|
35 |
version: tem[1] |
|
36 |
}; |
|
37 |
} |
|
38 |
} |
|
39 |
M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?']; |
|
40 |
if ((tem = ua.match(/version\/(\d+)/i)) != null) { |
|
41 |
M.splice(1, 1, tem[1]); |
|
42 |
} |
|
43 |
return { |
|
44 |
name: M[0], |
|
45 |
version: M[1] |
|
46 |
}; |
|
47 |
} |
|
48 | ||
49 |
function urlBase64ToUint8Array(base64String) { |
|
50 |
var padding = '='.repeat((4 - base64String.length % 4) % 4) |
|
51 |
var base64 = (base64String + padding) |
|
52 |
.replace(/\-/g, '+') |
|
53 |
.replace(/_/g, '/') |
|
54 | ||
55 |
var rawData = window.atob(base64) |
|
56 |
var outputArray = new Uint8Array(rawData.length) |
|
57 | ||
58 |
for (var i = 0; i < rawData.length; ++i) { |
|
59 |
outputArray[i] = rawData.charCodeAt(i) |
|
60 |
} |
|
61 |
return outputArray; |
|
62 |
} |
|
63 | ||
64 |
$(window) |
|
65 |
.load(function () { |
|
66 |
var gruPushSubscribeButton, |
|
67 |
gruPushMessageElt, |
|
68 |
gruSW; |
|
69 | ||
70 |
var activateTextMessage = 'Activez les notifications'; |
|
71 |
var stopTextMessage = 'Stoppez les notifications'; |
|
72 |
var incompatibleMessage = 'Ce navigateur n'est pas compatible avec les notifications push.'; |
|
73 | ||
74 |
gruPushSubscribeButton = $('#webpush-subscribe-checkbox'); |
|
75 |
gruPushMessageElt = $('#webpush-subscribe-message'); |
|
76 | ||
77 |
console.log("user has got webpush subscriptions :", gruPushSubscribeButton.data('userDevices')); |
|
78 | ||
79 |
function uncheckSubscribeButton() { |
|
80 |
gruPushSubscribeButton.attr('disabled', false); |
|
81 |
gruPushSubscribeButton.attr('checked', false); |
|
82 |
gruPushSubscribeButton.removeClass("checked"); |
|
83 |
gruPushMessageElt.text(activateTextMessage); |
|
84 |
} |
|
85 | ||
86 |
function checkSubscribeButton() { |
|
87 |
gruPushSubscribeButton.attr('disabled', false); |
|
88 |
gruPushSubscribeButton.attr('checked', true); |
|
89 |
gruPushSubscribeButton.addClass("checked"); |
|
90 |
gruPushMessageElt.text(stopTextMessage); |
|
91 |
} |
|
92 |
// disable if not supported |
|
93 |
if (!('serviceWorker' in navigator) || !('PushManager' in window)) { |
|
94 |
gruPushMessageElt.text(incompatibleMessage); |
|
95 |
gruPushSubscribeButton.attr('checked', false); |
|
96 |
gruPushSubscribeButton.attr('disabled', true); |
|
97 |
return; |
|
98 |
} |
|
99 |
// Get the initial subscription and refresh state from server |
|
100 |
if ('serviceWorker' in navigator) { |
|
101 |
var serviceWorkerSrc = '/service-worker.js'; |
|
102 |
navigator.serviceWorker.register(serviceWorkerSrc) |
|
103 |
.then(function (reg) { |
|
104 |
gruSW = reg; |
|
105 |
// Get the initial Subscription |
|
106 |
gruSW.pushManager.getSubscription() |
|
107 |
.then(function (subscription) { |
|
108 |
// Check we have a subscription to unsubscribe |
|
109 |
if (!subscription) { |
|
110 |
// No subscription object, so we uncheck |
|
111 |
uncheckSubscribeButton(); |
|
112 |
} else { |
|
113 |
// existing subscription object, so we check |
|
114 |
checkSubscribeButton(); |
|
115 |
} |
|
116 |
refreshSubscription(reg); |
|
117 |
}); |
|
118 |
}) |
|
119 |
.catch(function (err) { |
|
120 |
console.log('error registering service worker : ', err); |
|
121 |
}); |
|
122 |
} |
|
123 | ||
124 | ||
125 |
gruPushSubscribeButton.click( |
|
126 |
function () { |
|
127 |
gruPushMessageElt.text('Connexion au serveur en cours...'); |
|
128 |
gruPushSubscribeButton.attr('disabled', true); |
|
129 |
refreshSubscription(gruSW); |
|
130 |
} |
|
131 |
); |
|
132 | ||
133 | ||
134 |
// Once the service worker is registered set the initial state |
|
135 |
function refreshSubscription(reg) { |
|
136 |
// If its denied, it's a permanent block until the |
|
137 |
if (Notification.permission === 'denied') { |
|
138 |
// Show a message and uncheck the button |
|
139 |
uncheckSubscribeButton(); |
|
140 |
return; |
|
141 |
} |
|
142 |
// based on ":checked" being set before by pushManager.getSubscription() |
|
143 |
if (gruPushSubscribeButton.filter(':checked').length > 0) { |
|
144 |
return subscribe(reg); |
|
145 |
} else { |
|
146 |
return unsubscribe(reg); |
|
147 |
} |
|
148 |
} |
|
149 | ||
150 | ||
151 |
// get the Subscription or register one new to POST to our server |
|
152 |
function subscribe(reg) { |
|
153 |
getOrCreateSubscription(reg) |
|
154 |
.then(function (subscription) { |
|
155 |
postSubscribeObj(true, subscription); |
|
156 |
}) |
|
157 |
.catch(function (error) { |
|
158 |
gruPushMessageElt.text('Impossible de communiquer avec le serveur, veuillez retenter dans quelques minutes (Debug = ' + error + ')'); |
|
159 |
}); |
|
160 |
} |
|
161 | ||
162 |
function getOrCreateSubscription(reg) { |
|
163 |
return reg.pushManager.getSubscription() |
|
164 |
.then(function (subscription) { |
|
165 |
var applicationServerKey, options; |
|
166 |
// Check if a subscription is available |
|
167 |
if (subscription) { |
|
168 |
return subscription; |
|
169 |
} |
|
170 |
applicationServerKey = gruPushSubscribeButton.data('applicationServerKey'); |
|
171 |
options = { |
|
172 |
userVisibleOnly: true, // required by chrome |
|
173 |
applicationServerKey: urlBase64ToUint8Array(applicationServerKey) |
|
174 |
}; |
|
175 |
// If not, register one |
|
176 |
return reg.pushManager.subscribe(options) |
|
177 |
}) |
|
178 |
} |
|
179 | ||
180 |
function unsubscribe() { |
|
181 |
// Get the Subscription to unregister |
|
182 |
gruSW.pushManager.getSubscription() |
|
183 |
.then(function (subscription) { |
|
184 |
// Check we have a subscription to unsubscribe |
|
185 |
if (!subscription) { |
|
186 |
// No subscription object, so set the state |
|
187 |
// to allow the user to subscribe to push |
|
188 |
uncheckSubscribeButton(); |
|
189 |
return; |
|
190 |
} |
|
191 |
postSubscribeObj(false, subscription); |
|
192 |
}) |
|
193 |
} |
|
194 | ||
195 |
/* |
|
196 |
* Send the parameter to the server |
|
197 |
* the type of the request, the name of the user subscribing, |
|
198 |
* and the push subscription endpoint + key the server needs |
|
199 |
* Each subscription is different on different browsers |
|
200 |
*/ |
|
201 |
function postSubscribeObj(active, subscription) { |
|
202 |
subscription = subscription.toJSON() |
|
203 |
var browser = loadVersionBrowser(navigator.userAgent); |
|
204 |
var endpointParts = subscription.endpoint.split('/'); |
|
205 |
var registrationId = endpointParts[endpointParts.length - 1]; |
|
206 |
var data = { |
|
207 |
'browser': browser.name.toUpperCase(), |
|
208 |
'p256dh': subscription.keys.p256dh, |
|
209 |
'auth': subscription.keys.auth, |
|
210 |
'name': 'gru-notificationcell-subscription', |
|
211 |
'registration_id': registrationId, |
|
212 |
'active': active |
|
213 |
}; |
|
214 |
$.ajax({ |
|
215 |
url: gruPushSubscribeButton.data('comboApiUrl'), |
|
216 |
method: 'POST', |
|
217 |
data: JSON.stringify(data), |
|
218 |
dataType: 'json', |
|
219 |
crossDomain: true, |
|
220 |
cache: false, |
|
221 |
contentType: 'application/json; charset=UTF-8', |
|
222 |
xhrFields: { withCredentials: true } |
|
223 |
}) |
|
224 |
.done(function (response) { |
|
225 |
// Check if the parameter is saved on the server |
|
226 |
if (response.active) { |
|
227 |
// Show the unsubscribe button |
|
228 |
checkSubscribeButton(); |
|
229 |
} |
|
230 |
// Check if the information is deleted from server |
|
231 |
else if (!response.active) { |
|
232 |
// Get the Subscription |
|
233 |
getOrCreateSubscription(gruSW) |
|
234 |
.then(function (subscription) { |
|
235 |
// Remove the subscription |
|
236 |
subscription |
|
237 |
.unsubscribe() |
|
238 |
.then(function () { |
|
239 |
// Show the subscribe button |
|
240 |
uncheckSubscribeButton(); |
|
241 |
}); |
|
242 |
}) |
|
243 |
.catch(function (error) { |
|
244 |
gruPushSubscribeButton.attr('disabled', false); |
|
245 |
gruPushSubscribeButton.attr('checked', false); |
|
246 |
gruPushSubscribeButton.removeClass("checked"); |
|
247 |
gruPushMessageElt.text('Erreur lors de la requête, veuillez réessayer dans quelques minutes : ', error); |
|
248 |
}); |
|
249 |
} |
|
250 |
}) |
|
251 |
.fail(function (error) { |
|
252 |
gruPushMessageElt.text('Erreur lors de la requête, veuillez réessayer dans quelques minutes : ', error); |
|
253 |
}); |
|
254 |
} |
|
255 |
}); |
combo/apps/pwa/templates/combo/service-worker.js | ||
---|---|---|
1 |
{% load gadjo %} |
|
1 |
{% load gadjo static %}
|
|
2 | 2 |
/* global self, caches, fetch, URL, Response */ |
3 | 3 |
'use strict'; |
4 | 4 | |
5 | 5 |
var config = { |
6 |
version: 'v{% start_timestamp %}', |
|
7 |
staticCacheItems: [ |
|
8 |
'/offline/' |
|
9 |
], |
|
10 |
cachePathPattern: /^\/static\/.*/, |
|
11 |
handleFetchPathPattern: /.*/, |
|
12 |
offlinePage: '/offline/' |
|
6 |
version: 'v{% start_timestamp %}', |
|
7 |
staticCacheItems: [], // putting 404 items fail its registration (will never be "installed") |
|
8 |
cachePathPattern: /^\/static\/.*/, |
|
9 |
handleFetchPathPattern: /.*/, |
|
10 |
offlinePage: '/offline/' |
|
13 | 11 |
}; |
14 | 12 | |
15 | 13 |
function cacheName (key, opts) { |
16 |
return `${opts.version}-${key}`;
|
|
14 |
return `${opts.version}-${key}`;
|
|
17 | 15 |
} |
18 | 16 | |
19 | 17 |
function addToCache (cacheKey, request, response) { |
20 |
if (response.ok && cacheKey !== null) {
|
|
21 |
var copy = response.clone();
|
|
22 |
caches.open(cacheKey).then( cache => {
|
|
23 |
cache.put(request, copy);
|
|
24 |
});
|
|
25 |
}
|
|
26 |
return response;
|
|
18 |
if (response.ok && cacheKey !== null) {
|
|
19 |
var copy = response.clone();
|
|
20 |
caches.open(cacheKey).then( cache => {
|
|
21 |
cache.put(request, copy);
|
|
22 |
});
|
|
23 |
}
|
|
24 |
return response;
|
|
27 | 25 |
} |
28 | 26 | |
29 | 27 |
function fetchFromCache (event) { |
30 |
return caches.match(event.request).then(response => {
|
|
31 |
if (!response) {
|
|
32 |
throw Error(`${event.request.url} not found in cache`);
|
|
33 |
}
|
|
34 |
return response;
|
|
35 |
});
|
|
28 |
return caches.match(event.request).then(response => {
|
|
29 |
if (!response) {
|
|
30 |
throw Error(`${event.request.url} not found in cache`);
|
|
31 |
}
|
|
32 |
return response;
|
|
33 |
});
|
|
36 | 34 |
} |
37 | 35 | |
38 | 36 |
function offlineResponse (resourceType, opts) { |
39 |
if (resourceType === 'content') {
|
|
40 |
return caches.match(opts.offlinePage);
|
|
41 |
}
|
|
42 |
return undefined;
|
|
37 |
if (resourceType === 'content') {
|
|
38 |
return caches.match(opts.offlinePage);
|
|
39 |
}
|
|
40 |
return undefined;
|
|
43 | 41 |
} |
44 | 42 | |
45 | 43 |
self.addEventListener('install', event => { |
46 |
function onInstall (event, opts) {
|
|
47 |
var cacheKey = cacheName('static', opts);
|
|
48 |
return caches.open(cacheKey)
|
|
49 |
.then(cache => cache.addAll(opts.staticCacheItems));
|
|
50 |
}
|
|
51 | ||
52 |
event.waitUntil(
|
|
53 |
onInstall(event, config).then( () => self.skipWaiting() )
|
|
54 |
);
|
|
44 |
function onInstall (event, opts) {
|
|
45 |
var cacheKey = cacheName('static', opts);
|
|
46 |
return caches.open(cacheKey)
|
|
47 |
.then(cache => cache.addAll(opts.staticCacheItems));
|
|
48 |
}
|
|
49 | ||
50 |
event.waitUntil(
|
|
51 |
onInstall(event, config).then( () => self.skipWaiting() )
|
|
52 |
);
|
|
55 | 53 |
}); |
56 | 54 | |
57 | 55 |
self.addEventListener('activate', event => { |
58 |
function onActivate (event, opts) {
|
|
59 |
return caches.keys()
|
|
60 |
.then(cacheKeys => {
|
|
61 |
var oldCacheKeys = cacheKeys.filter(key => key.indexOf(opts.version) !== 0);
|
|
62 |
var deletePromises = oldCacheKeys.map(oldKey => caches.delete(oldKey));
|
|
63 |
return Promise.all(deletePromises);
|
|
64 |
});
|
|
65 |
}
|
|
66 | ||
67 |
event.waitUntil(
|
|
68 |
onActivate(event, config)
|
|
69 |
.then( () => self.clients.claim() )
|
|
70 |
);
|
|
56 |
function onActivate (event, opts) {
|
|
57 |
return caches.keys()
|
|
58 |
.then(cacheKeys => {
|
|
59 |
var oldCacheKeys = cacheKeys.filter(key => key.indexOf(opts.version) !== 0);
|
|
60 |
var deletePromises = oldCacheKeys.map(oldKey => caches.delete(oldKey));
|
|
61 |
return Promise.all(deletePromises);
|
|
62 |
});
|
|
63 |
}
|
|
64 | ||
65 |
event.waitUntil(
|
|
66 |
onActivate(event, config)
|
|
67 |
.then( () => self.clients.claim() )
|
|
68 |
);
|
|
71 | 69 |
}); |
72 | 70 | |
73 | 71 |
self.addEventListener('fetch', event => { |
74 | 72 | |
75 |
function shouldHandleFetch (event, opts) { |
|
76 |
var request = event.request; |
|
77 |
var url = new URL(request.url); |
|
78 |
var criteria = { |
|
79 |
matchesPathPattern: opts.handleFetchPathPattern.test(url.pathname), |
|
80 |
isGETRequest : request.method === 'GET', |
|
81 |
isFromMyOrigin : url.origin === self.location.origin |
|
82 |
}; |
|
83 |
var failingCriteria = Object.keys(criteria) |
|
84 |
.filter(criteriaKey => !criteria[criteriaKey]); |
|
85 |
return !failingCriteria.length; |
|
86 |
} |
|
87 | ||
88 |
function onFetch (event, opts) { |
|
89 |
var request = event.request; |
|
90 |
var url = new URL(request.url); |
|
91 |
var acceptHeader = request.headers.get('Accept'); |
|
92 |
var resourceType = 'static'; |
|
93 |
var cacheKey; |
|
94 | ||
95 |
if (acceptHeader.indexOf('text/html') !== -1) { |
|
96 |
resourceType = 'content'; |
|
97 |
} else if (acceptHeader.indexOf('image') !== -1) { |
|
98 |
resourceType = 'image'; |
|
99 |
} |
|
100 | ||
101 |
cacheKey = null; |
|
102 |
if (opts.cachePathPattern.test(url.pathname)) { |
|
103 |
cacheKey = cacheName(resourceType, opts); |
|
104 |
} |
|
105 | ||
106 |
/* always network first */ |
|
107 |
event.respondWith( |
|
108 |
fetch(request) |
|
109 |
.then(response => addToCache(cacheKey, request, response)) |
|
110 |
.catch(() => fetchFromCache(event)) |
|
111 |
.catch(() => offlineResponse(resourceType, opts)) |
|
112 |
); |
|
113 |
} |
|
114 |
if (shouldHandleFetch(event, config)) { |
|
115 |
onFetch(event, config); |
|
116 |
} |
|
73 |
function shouldHandleFetch (event, opts) { |
|
74 |
var request = event.request; |
|
75 |
var url = new URL(request.url); |
|
76 |
var criteria = { |
|
77 |
matchesPathPattern: opts.handleFetchPathPattern.test(url.pathname), |
|
78 |
isGETRequest : request.method === 'GET', |
|
79 |
isFromMyOrigin : url.origin === self.location.origin |
|
80 |
}; |
|
81 |
var failingCriteria = Object.keys(criteria) |
|
82 |
.filter(criteriaKey => !criteria[criteriaKey]); |
|
83 |
return !failingCriteria.length; |
|
84 |
} |
|
85 | ||
86 |
function onFetch (event, opts) { |
|
87 |
var request = event.request; |
|
88 |
var url = new URL(request.url); |
|
89 |
var acceptHeader = request.headers.get('Accept'); |
|
90 |
var resourceType = 'static'; |
|
91 |
var cacheKey; |
|
92 | ||
93 |
if (acceptHeader.indexOf('text/html') !== -1) { |
|
94 |
resourceType = 'content'; |
|
95 |
} else if (acceptHeader.indexOf('image') !== -1) { |
|
96 |
resourceType = 'image'; |
|
97 |
} |
|
98 | ||
99 |
cacheKey = null; |
|
100 |
if (opts.cachePathPattern.test(url.pathname)) { |
|
101 |
cacheKey = cacheName(resourceType, opts); |
|
102 |
} |
|
103 | ||
104 |
/* always network first */ |
|
105 |
event.respondWith( |
|
106 |
fetch(request) |
|
107 |
.then(response => addToCache(cacheKey, request, response)) |
|
108 |
.catch(() => fetchFromCache(event)) |
|
109 |
.catch(() => offlineResponse(resourceType, opts)) |
|
110 |
); |
|
111 |
} |
|
112 |
if (shouldHandleFetch(event, config)) { |
|
113 |
onFetch(event, config); |
|
114 |
} |
|
115 |
}); |
|
116 | ||
117 |
/* |
|
118 |
* Return the options paramter for showNotification |
|
119 |
* Only Chrome has extended support for extra features like actions, badge, icon, etc |
|
120 |
* https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification |
|
121 |
*/ |
|
122 |
var getNotificationOptions = function (responseJson) { |
|
123 |
var body = responseJson.body, |
|
124 |
icon = responseJson.icon, |
|
125 |
vibrate = responseJson.vibrate, |
|
126 |
data = responseJson.data, |
|
127 |
actions = responseJson.actions; |
|
128 |
/* default icon is configured in the theme 'css_variant' */ |
|
129 |
if (!icon ) icon = '{{ site_base }}{% static "" %}{{ css_variant }}/{{ icon_prefix }}512px.png'; |
|
130 |
var options = { |
|
131 |
body: body, |
|
132 |
icon: icon, |
|
133 |
requireInteraction: 'true', |
|
134 |
data: data, |
|
135 |
actions: actions |
|
136 |
}; |
|
137 |
/* optional vibration */ |
|
138 |
if (vibrate) options.vibrate = vibrate; |
|
139 |
return options; |
|
140 |
}; |
|
141 | ||
142 |
/* |
|
143 |
* Push event handler |
|
144 |
* documentation at https://developers.google.com/web/fundamentals/push-notifications/display-a-notification |
|
145 |
*/ |
|
146 |
self.addEventListener('push', function (event) { |
|
147 |
try { |
|
148 |
// Push is a JSON |
|
149 |
var responseJson = event.data.json(); |
|
150 |
var title = responseJson.title; |
|
151 |
var options = getNotificationOptions(responseJson) |
|
152 |
} catch (err) { |
|
153 |
// Push is a simple text (usually debugging) |
|
154 |
console.log('Push is a simple text'); |
|
155 |
var options = { |
|
156 |
'body': event.data.text() |
|
157 |
}; |
|
158 |
var title = ''; |
|
159 |
} |
|
160 |
event.waitUntil(self.registration.showNotification(title, options)); |
|
161 |
}); |
|
162 | ||
163 |
self.addEventListener('notificationclick', function (event) { |
|
164 |
if (!event.action) { |
|
165 |
// Was a normal notification click |
|
166 |
console.log('No actions'); |
|
167 |
} |
|
168 |
var urlToOpen = event.notification.data.open_url; |
|
169 |
switch (event.action) { |
|
170 |
case 'ack': |
|
171 |
break; |
|
172 |
case 'forget': |
|
173 |
break; |
|
174 |
} |
|
175 |
if (event.action) { |
|
176 |
// ack or forget |
|
177 |
fetch(event.notification.data.callback_url + '?action=' + event.action, { |
|
178 |
method: 'GET', |
|
179 |
mode: 'cors', // no-cors, cors, *same-origin |
|
180 |
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached |
|
181 |
}) |
|
182 |
.catch(function() { |
|
183 |
console.log("error on GET ", event.notification.data.callback_url + '?action=' + event.action); |
|
184 |
}); |
|
185 |
} |
|
186 |
// Check if there's already a tab open with this urlToOpen. |
|
187 |
event.waitUntil(self.clients.matchAll({ |
|
188 |
type: 'window', |
|
189 |
includeUncontrolled: true |
|
190 |
}) |
|
191 |
.then(function (windowClients) { |
|
192 |
let matchingClient = null; |
|
193 |
for (let i = 0; i < windowClients.length; i++) { |
|
194 |
const windowClient = windowClients[i]; |
|
195 |
if (windowClient.url === urlToOpen) { |
|
196 |
matchingClient = windowClient; |
|
197 |
break; |
|
198 |
} |
|
199 |
} |
|
200 |
if (matchingClient) { |
|
201 |
return matchingClient.focus(); |
|
202 |
} else { |
|
203 |
return clients.openWindow(urlToOpen); |
|
204 |
} |
|
205 |
}) |
|
206 |
); |
|
207 |
// Android doesn't close the notification when you click it |
|
208 |
// See http://crbug.com/463146 |
|
209 |
event.notification.close(); |
|
117 | 210 |
}); |
combo/apps/pwa/urls.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2015-2018 Entr'ouvert |
|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# combo - Combo PWA App |
|
3 |
# Copyright (C) 2018 Entr'ouvert |
|
3 | 4 |
# |
4 | 5 |
# This program is free software: you can redistribute it and/or modify it |
5 | 6 |
# under the terms of the GNU Affero General Public License as published |
... | ... | |
16 | 17 | |
17 | 18 |
from django.conf.urls import url |
18 | 19 | |
19 |
from .views import manifest_json, service_worker_js |
|
20 |
from .views import manifest_json, service_worker_js, WebPushDeviceAuthorizedViewSetNoCsrf, NotificationCallback |
|
21 | ||
20 | 22 | |
21 | 23 |
urlpatterns = [ |
22 | 24 |
url('^manifest.json$', manifest_json), |
23 | 25 |
url('^service-worker.js$', service_worker_js), |
24 |
] |
|
26 |
url('^webpush/subscribe/$', WebPushDeviceAuthorizedViewSetNoCsrf.as_view({'post': 'create'}), name='create_webpush_subscription'), |
|
27 |
url(r'^webpush/notification/(\w+)/(\w+)/(\S+)/$', NotificationCallback.as_view(), name='webpush-notification-callback'), |
|
28 |
] |
combo/apps/pwa/views.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2015-2018 Entr'ouvert |
|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# combo - Combo PWA App |
|
3 |
# Copyright (C) 2018 Entr'ouvert |
|
4 |
# |
|
5 |
# This program is free software: you can redistribute it and/or modify it |
|
6 |
# under the terms of the GNU Affero General Public License as published |
|
7 |
# by the Free Software Foundation, either version 3 of the License, or |
|
8 |
# (at your option) any later version. |
|
9 |
# |
|
10 |
# This program is distributed in the hope that it will be useful, |
|
11 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
12 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
13 |
# GNU Affero General Public License for more details. |
|
14 |
# |
|
15 |
# You should have received a copy of the GNU Affero General Public License |
|
16 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
17 | ||
18 |
# combo - Combo PWA App |
|
19 |
# Copyright (C) 2018 Entr'ouvert |
|
3 | 20 |
# |
4 | 21 |
# This program is free software: you can redistribute it and/or modify it |
5 | 22 |
# under the terms of the GNU Affero General Public License as published |
... | ... | |
17 | 34 |
from django.http import HttpResponse, Http404 |
18 | 35 |
from django.template.loader import get_template, TemplateDoesNotExist |
19 | 36 | |
37 |
from rest_framework import permissions, status |
|
38 |
from rest_framework.generics import GenericAPIView |
|
39 |
from rest_framework.response import Response |
|
40 |
from rest_framework.authentication import SessionAuthentication |
|
41 | ||
42 |
from push_notifications.models import WebPushDevice |
|
43 |
from push_notifications.api.rest_framework import WebPushDeviceAuthorizedViewSet |
|
44 | ||
45 |
from combo.apps.notifications.models import Notification |
|
46 | ||
20 | 47 | |
21 | 48 |
def manifest_json(request, *args, **kwargs): |
22 | 49 |
try: |
... | ... | |
29 | 56 |
def service_worker_js(request, *args, **kwargs): |
30 | 57 |
template = get_template('combo/service-worker.js') |
31 | 58 |
return HttpResponse(template.render({}, request), |
32 |
content_type='application/javascript; charset=utf-8') |
|
59 |
content_type='application/javascript; charset=utf-8') |
|
60 | ||
61 | ||
62 |
class CsrfExemptSessionAuthentication(SessionAuthentication): |
|
63 |
def enforce_csrf(self, request): |
|
64 |
return # To not perform the csrf check previously happening |
|
65 | ||
66 | ||
67 |
class WebPushDeviceAuthorizedViewSetNoCsrf(WebPushDeviceAuthorizedViewSet): |
|
68 |
authentication_classes = (CsrfExemptSessionAuthentication,) |
|
69 | ||
70 | ||
71 |
class NotificationCallback(GenericAPIView): |
|
72 |
''' |
|
73 |
Ack or forget a Notification object |
|
74 |
Anonymously but with a check on the user's public webpush registration key |
|
75 |
''' |
|
76 |
permission_classes = (permissions.AllowAny,) |
|
77 |
authentication_classes = (CsrfExemptSessionAuthentication,) |
|
78 | ||
79 |
def get(self, request, notification_id, user, key, *args, **kwargs): |
|
80 |
action = request.GET['action'] |
|
81 |
try: |
|
82 |
WebPushDevice.objects.get(p256dh=key) |
|
83 |
qs = Notification.objects.find(user, notification_id) |
|
84 |
if action == 'ack': |
|
85 |
qs.ack() |
|
86 |
if action == 'forget': |
|
87 |
qs.forget() |
|
88 |
return Response({'err': 0}) |
|
89 |
except WebPushDevice.DoesNotExist: |
|
90 |
return Response({'err': 1}, status.HTTP_400_BAD_REQUEST) |
|
91 | ||
92 | ||
93 |
notification_callback = NotificationCallback.as_view() |
|
33 |
- |