View source
<?php
define('PWA_WORKBOX_URL', 'https://storage.googleapis.com/workbox-cdn/releases/5.1.4/workbox-sw.js');
define('PWA_SW_CACHE_EXCLUDE', "^/admin/.*\n^/user/reset/.*");
define('PWA_SW_ASSETS', [
'image' => [
'strategy' => 'CacheFirst',
'limitMaxSize' => TRUE,
'maxSize' => 350,
'external' => FALSE,
],
'script' => [
'strategy' => 'StaleWhileRevalidate',
'limitMaxSize' => FALSE,
'maxSize' => 500,
'external' => TRUE,
],
'style' => [
'strategy' => 'StaleWhileRevalidate',
'limitMaxSize' => FALSE,
'maxSize' => 300,
'external' => TRUE,
],
'font' => [
'strategy' => 'CacheFirst',
'limitMaxSize' => FALSE,
'maxSize' => 1000,
'external' => TRUE,
],
]);
require __DIR__ . '/includes/pwa.apple.inc';
function pwa_permission() {
return [
'access pwa' => [
'title' => t('Access Progressive Web App'),
],
'administer pwa manifest' => [
'title' => t('Administer Progressive Web App manifest configuration'),
],
'administer pwa serviceworker' => [
'title' => t('Administer Progressive Web App serviceworker configuration'),
'restrict access' => TRUE,
],
];
}
function pwa_menu() {
$items = [];
$items['pwa/storage/clear'] = [
'page callback' => 'pwa_serviceworker_clear',
'access arguments' => [
'access pwa',
],
'type' => MENU_CALLBACK,
];
$items['pwa/serviceworker/check'] = [
'page callback' => 'pwa_module_active',
'access callback' => '_pwa_access_check',
'delivery callback' => 'pwa_deliver_json',
'file' => 'pwa.pages.inc',
'type' => MENU_CALLBACK,
];
$items['pwa/serviceworker/js'] = [
'page callback' => 'pwa_file_data',
'page arguments' => [
'serviceworker',
],
'access callback' => '_pwa_access_check',
'delivery callback' => 'pwa_deliver_serviceworker_file',
'file' => 'pwa.pages.inc',
'type' => MENU_CALLBACK,
];
$items['manifest.webmanifest'] = [
'page callback' => 'pwa_file_data',
'page arguments' => [
'manifest',
],
'access callback' => TRUE,
'delivery callback' => 'pwa_deliver_manifest_file',
'file' => 'pwa.pages.inc',
'type' => MENU_CALLBACK,
];
$items['offline'] = [
'page callback' => 'pwa_offline_page',
'access callback' => '_pwa_access_check',
'file' => 'pwa.pages.inc',
'type' => MENU_CALLBACK,
];
$items['admin/config/pwa'] = [
'title' => t('Progressive Web App'),
'description' => t('Progressive Web App configuration.'),
'position' => 'right',
'page callback' => 'system_admin_menu_block_page',
'access arguments' => [
'access administration pages',
],
'file' => 'system.admin.inc',
'file path' => drupal_get_path('module', 'system'),
'weight' => 10,
];
$items['admin/config/pwa/settings'] = [
'title' => t('PWA information'),
'description' => t('Control how your website appears on mobile devices when used as a PWA.'),
'page callback' => 'drupal_get_form',
'page arguments' => [
'pwa_admin_configuration',
],
'access arguments' => [
'administer pwa manifest',
],
'file' => 'pwa.admin.inc',
'type' => MENU_NORMAL_ITEM,
'weight' => -10,
];
$items['admin/config/pwa/settings/main'] = [
'title' => t('Add to Homescreen'),
'type' => MENU_DEFAULT_LOCAL_TASK,
] + $items['admin/config/pwa/settings'];
$items['admin/config/pwa/settings/manifest'] = [
'title' => t('Manifest'),
'page arguments' => [
'pwa_admin_configuration_manifest',
],
'type' => MENU_LOCAL_TASK,
] + $items['admin/config/pwa/settings'];
$items['admin/config/pwa/settings/serviceworker'] = [
'title' => t('ServiceWorker'),
'page arguments' => [
'pwa_admin_configuration_sw',
],
'access arguments' => [
'administer pwa serviceworker',
],
'type' => MENU_LOCAL_TASK,
] + $items['admin/config/pwa/settings'];
$items['admin/config/pwa/cache'] = [
'title' => t('Offline Cache'),
'description' => t('Configure offline cache and caching strategies.'),
'page callback' => 'drupal_get_form',
'page arguments' => [
'pwa_admin_configuration_sw_patterns',
],
'access arguments' => [
'administer pwa serviceworker',
],
'file' => 'pwa.admin.inc',
'type' => MENU_NORMAL_ITEM,
];
$items['admin/config/pwa/cache/patterns'] = [
'title' => t('Cache patterns & strategies'),
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -1,
] + $items['admin/config/pwa/cache'];
$items['admin/config/pwa/cache/precache'] = [
'title' => t('Precaching'),
'page arguments' => [
'pwa_admin_configuration_sw_precache',
],
'type' => MENU_LOCAL_TASK,
] + $items['admin/config/pwa/cache'];
return $items;
}
function pwa_version_assets($module) {
$module_info = system_get_info('module', $module);
$version = $module_info['version'] ?? 'dev';
if (strpos($version, 'dev') !== FALSE) {
$version .= '-' . $module_info['mtime'];
}
return $version;
}
function pwa_library() {
$path = drupal_get_path('module', 'pwa');
$module_version = pwa_version_assets('pwa');
return [
'register' => [
'version' => $module_version,
'js' => [
$path . '/js/register.js' => [
'scope' => 'footer',
],
$path . '/js/autoreload.js' => [
'scope' => 'footer',
],
$path . '/js/beforeinstallprompt.js' => [
'scope' => 'footer',
],
0 => [
'data' => [
'pwa' => [
'path' => url('/pwa/serviceworker/js'),
],
],
'type' => 'setting',
],
],
'dependencies' => [],
],
'sha256' => [
'version' => $module_version,
'js' => [
$path . '/js/sha256.js' => [
'scope' => 'footer',
],
],
],
];
}
function _pwa_access_check($permission = '') {
if (user_access('access pwa')) {
if (!empty($permission)) {
return user_access($permission);
}
return TRUE;
}
return FALSE;
}
function pwa_file_data($file) {
$function = '_pwa_' . $file . '_file';
if (!in_array($file, [
'manifest',
'serviceworker',
]) || !function_exists($function)) {
return FALSE;
}
$cid = 'pwa:' . $file;
$data = cache_get($cid, 'cache');
if ($data) {
$data = $data->data;
}
else {
$data = $function();
cache_set($cid, $data, 'cache');
}
return $data;
}
function _pwa_manifest_file() {
$path = drupal_get_path('module', 'pwa');
global $language;
$manifest = [
'name' => variable_get('pwa_name', variable_get('site_name')),
'short_name' => variable_get('pwa_short_name', variable_get('site_name')),
'description' => variable_get('pwa_description', ''),
'lang' => $language->language,
'dir' => $language->direction == LANGUAGE_LTR ? 'ltr' : 'rtl',
'background_color' => variable_get('pwa_background_color', '#F6F6F2'),
'theme_color' => variable_get('pwa_theme_color', '#53B0EB'),
'start_url' => variable_get('pwa_start_url', '/?source=pwa'),
'orientation' => variable_get('pwa_orientation', 'portrait'),
'display' => variable_get('pwa_display', 'standalone'),
'icons' => [
[
'src' => file_create_url($path . '/assets/drupal-192.png'),
'sizes' => '192x192',
'type' => 'image/png',
'purpose' => 'any maskable',
],
[
'src' => file_create_url($path . '/assets/drupal-512.png'),
'sizes' => '512x512',
'type' => 'image/png',
'purpose' => 'any maskable',
],
[
'src' => file_create_url($path . '/assets/drupal.svg'),
'type' => 'image/svg+xml',
'sizes' => '512x512',
'purpose' => 'any maskable',
],
[
'src' => file_create_url($path . '/assets/drupal-black.svg'),
'type' => 'image/svg+xml',
'sizes' => '16x16',
'purpose' => 'monochrome',
],
],
];
drupal_alter('pwa_manifest', $manifest);
return $manifest;
}
function _pwa_serviceworker_file() {
$path = drupal_get_path('module', 'pwa');
$sw = file_get_contents($path . '/js/serviceworker/_template.js');
$precache_page = _pwa_config_value_split('pwa_sw_precache_page', '');
$precache_asset = _pwa_config_value_split('pwa_sw_precache_asset', '');
$offline_url = url('/offline');
$offline_image = file_create_url($path . '/assets/offline-image.png');
$pwa_module_version = pwa_version_assets('pwa');
$replace = [];
$drupalPWASettings = [
'version' => $pwa_module_version . '-v' . variable_get('pwa_sw_cache_version', 1),
'debug' => (bool) variable_get('pwa_sw_debug', FALSE),
'cache' => [
'exclude' => array_merge(_pwa_config_value_split('pwa_sw_cache_exclude', PWA_SW_CACHE_EXCLUDE), [
'^/pwa/.*',
]),
'precache' => [
'page' => array_merge($precache_page, [
variable_get('pwa_start_url', '/?source=pwa'),
$offline_url,
]),
'asset' => array_merge($precache_asset, [
$offline_image,
]),
],
'patterns' => [
'page' => _pwa_config_value_explode('pwa_sw_patterns_page', ''),
'asset' => _pwa_config_value_explode('pwa_sw_patterns_asset', ''),
],
'assets' => variable_get('pwa_sw_asset_config', PWA_SW_ASSETS),
'offline' => [
'page' => $offline_url,
'image' => $offline_image,
],
],
];
if (variable_get('pwa_sw_phonehome', TRUE)) {
$drupalPWASettings['phonehome'] = [
'idle' => 10 * 60,
'count' => 10,
];
}
drupal_alter('pwa_serviceworker_data', $drupalPWASettings);
$replace['{/*drupalPWASettings*/}'] = json_encode($drupalPWASettings, JSON_PRETTY_PRINT | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
$workbox_url = variable_get('pwa_workbox_url');
$scripts = [
'utils' => $path . '/js/serviceworker/utils.js',
'phonehome' => variable_get('pwa_sw_phonehome', TRUE) ? $path . '/js/serviceworker/phonehome.js' : NULL,
'workbox' => !empty($workbox_url) ? $workbox_url : PWA_WORKBOX_URL,
'cache' => $path . '/js/serviceworker/cache.js',
];
drupal_alter('pwa_serviceworker_script', $scripts);
$importScripts = [];
foreach (array_filter($scripts) as $script) {
$importScripts[] = "importScripts('" . url($script, [
'query' => [
'v' => $pwa_module_version,
],
'absolute' => TRUE,
]) . "');";
}
$replace['/*importScripts*/'] = implode("\n", $importScripts);
return str_replace(array_keys($replace), array_values($replace), $sw);
}
function _pwa_config_value_split($variable, $default = '') {
$value = variable_get($variable, $default);
return array_filter((array) preg_split("/\r\n|\n|\r/", trim($value)));
}
function _pwa_config_value_explode($value, $default = '') {
$data = _pwa_config_value_split($value, $default);
$result = [];
foreach ($data as $config) {
list($strategy, $value) = explode('|', trim($config), 2);
$result[] = [
'strategy' => $strategy,
'value' => $value,
];
}
return $result;
}
function pwa_html_head_alter(&$head_elements) {
$has_viewport_meta = FALSE;
foreach ($head_elements as $key => $element) {
if (!empty($element['#tag']) && $element['#tag'] == 'meta' && (!empty($element['#attributes']['name']) && $element['#attributes']['name'] == 'viewport')) {
$has_viewport_meta = TRUE;
break;
}
}
if (!$has_viewport_meta) {
$head_elements['viewport'] = [
'#type' => 'html_tag',
'#tag' => 'meta',
'#attributes' => [
'name' => 'viewport',
'content' => 'width=device-width, initial-scale=1',
],
'#weight' => 10,
];
}
if (!variable_get('pwa_apple_meta_enable', TRUE)) {
return;
}
_pwa_apple_html_head_alter($head_elements);
}
function pwa_preprocess_html(&$variables) {
if (_pwa_access_check() && variable_get('pwa_sw_everywhere', FALSE)) {
$block = pwa_block_view('pwa_register');
$library = $block['content']['#attached']['library'][0];
$settings = $block['content']['#attached']['js'][0];
drupal_add_library($library[0], $library[1]);
drupal_add_js($settings['data'], $settings['type']);
}
$manifest = pwa_file_data('manifest');
drupal_add_html_head([
'#tag' => 'link',
'#attributes' => [
'rel' => 'manifest',
'href' => '/manifest.webmanifest?v=' . variable_get('pwa_sw_cache_version', 1),
'crossorigin' => user_is_logged_in() ? 'use-credentials' : '',
],
'#weight' => 10,
], 'manifest');
drupal_add_html_head([
'#tag' => 'meta',
'#attributes' => [
'name' => 'theme-color',
'content' => $manifest['theme_color'],
],
'#weight' => 10,
], 'theme_color');
if ($icon_src = array_search('192x192', array_column($manifest['icons'], 'sizes', 'src'))) {
drupal_add_html_head([
'#tag' => 'link',
'#attributes' => [
'rel' => 'apple-touch-icon',
'href' => $icon_src,
],
'#weight' => 80,
], 'apple-touch-icon');
}
}
function pwa_modernizr_info() {
return [
'serviceworker',
];
}
function pwa_user_logout($account) {
drupal_add_http_header('Clear-Site-Data', '"storage", "cookies"');
}
function pwa_user_login(&$edit, $account) {
drupal_add_http_header('Clear-Site-Data', '"storage"');
}
function pwa_serviceworker_clear() {
drupal_add_http_header('Clear-Site-Data', '"storage"');
drupal_goto('<front>');
}
function _pwa_check_page_callback_result($page_callback_result) {
if (is_int($page_callback_result)) {
switch ($page_callback_result) {
case MENU_ACCESS_DENIED:
drupal_add_http_header('Content-Type', 'application/javascript');
drupal_add_http_header('Status', '403 Forbidden');
print drupal_json_encode([
'pwa' => 'access denied',
]);
return FALSE;
break;
case MENU_NOT_FOUND:
drupal_add_http_header('Content-Type', 'application/javascript');
drupal_add_http_header('Status', '404 Not Found');
print drupal_json_encode([
'pwa' => 'not found',
]);
return FALSE;
break;
case MENU_SITE_OFFLINE:
drupal_add_http_header('Content-Type', 'application/javascript');
drupal_add_http_header('Status', '503 Service unavailable');
print drupal_json_encode([
'pwa' => 'site under maintenance',
]);
return FALSE;
break;
}
}
return TRUE;
}
function pwa_deliver_json($page_callback_result) {
if (_pwa_check_page_callback_result($page_callback_result)) {
drupal_add_http_header('Content-Type', 'application/json');
print drupal_json_encode($page_callback_result);
}
ajax_footer();
}
function pwa_deliver_serviceworker_file($page_callback_result) {
if (_pwa_check_page_callback_result($page_callback_result)) {
drupal_add_http_header('Content-Type', 'application/javascript');
drupal_add_http_header('Content-Disposition', 'inline; filename="serviceworker.js"');
drupal_add_http_header('Service-Worker-Allowed', base_path());
drupal_add_http_header('Cache-Control', 'public, max-age=604800');
print $page_callback_result;
}
}
function pwa_deliver_manifest_file($page_callback_result) {
if (_pwa_check_page_callback_result($page_callback_result)) {
drupal_add_http_header('Content-Type', 'application/manifest+json');
drupal_add_http_header('Content-Disposition', 'inline; filename="manifest.webmanifest"');
drupal_add_http_header('Cache-Control', 'max-age=604800');
print json_encode($page_callback_result, JSON_PRETTY_PRINT | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
}
}
function pwa_block_info() {
$blocks = [];
$blocks['pwa_register'] = [
'info' => t('PWA register serviceworker'),
'weight' => 100,
'status' => 1,
'region' => 'footer',
'visibility' => BLOCK_VISIBILITY_NOTLISTED,
'pages' => variable_get('pwa_serviceworker_cache_exclude', PWA_SW_CACHE_EXCLUDE),
];
return $blocks;
}
function pwa_block_view($delta = '') {
$block = [];
if ($delta === 'pwa_register') {
$block['content'] = [
'#access' => _pwa_access_check(),
'#attached' => [
'library' => [
[
'pwa',
'register',
],
],
'js' => [
[
'data' => [
'pwa' => [
'path' => url('pwa/serviceworker/js'),
],
],
'type' => 'setting',
],
],
],
];
}
return $block;
}
function pwa_block_configure($delta = '') {
$form = [];
if ($delta === 'pwa_register') {
$form['pwa_block_register_informations'] = [
'#type' => 'markup',
'#value' => t('Add this block to include registration script of the serviceworker.'),
];
}
return $form;
}