You are here

pwa.module in Progressive Web App 7.2

Same filename and directory in other branches
  1. 8 pwa.module
  2. 7 pwa.module
  3. 2.x pwa.module

File

pwa.module
View source
<?php

/**
 * @file
 */
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';

/**
 * Implements hook_permission().
 */
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,
    ],
  ];
}

/**
 * Implements hook_menu().
 */
function pwa_menu() {
  $items = [];

  // Clear the user storage with the Site-Data-Clear header.
  // Use the ?destination parameter to redirect the user to a page after
  // clearing the storage.
  $items['pwa/storage/clear'] = [
    'page callback' => 'pwa_serviceworker_clear',
    'access arguments' => [
      'access pwa',
    ],
    'type' => MENU_CALLBACK,
  ];

  // Callback for the phone home feature.
  $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,
  ];

  // The JS of the serviceworker file.
  $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,
  ];

  // Specify manifest settings as default landing page when using system nav.
  $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,
  ];

  // Admin settings for manifest.json
  $items['admin/config/pwa/settings/main'] = [
    'title' => t('Add to Homescreen'),
    'type' => MENU_DEFAULT_LOCAL_TASK,
  ] + $items['admin/config/pwa/settings'];

  // Admin settings for manifest.json
  $items['admin/config/pwa/settings/manifest'] = [
    'title' => t('Manifest'),
    'page arguments' => [
      'pwa_admin_configuration_manifest',
    ],
    'type' => MENU_LOCAL_TASK,
  ] + $items['admin/config/pwa/settings'];

  // Admin settings for manifest.json
  $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'];

  // Admin settings to control how SW is loaded and cached.
  $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;
}

/**
 * Return a cache busting parameter for assets.
 *
 * @param $module
 *
 * @return mixed|string
 */
function pwa_version_assets($module) {
  $module_info = system_get_info('module', $module);

  // Use version info to prevent issues with caching proxies.
  $version = $module_info['version'] ?? 'dev';

  // On dev release make sure it's different than the generic -dev version.
  if (strpos($version, 'dev') !== FALSE) {
    $version .= '-' . $module_info['mtime'];
  }
  return $version;
}

/**
 * Implements hook_library().
 */
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',
        ],
      ],
    ],
  ];
}

/**
 * Custom access function to force the exclusion of the super admin for
 * all serviceworker related things.
 */
function _pwa_access_check($permission = '') {

  // The user always need the 'access pwa' permission.
  if (user_access('access pwa')) {

    // Check an extra permission if necessary.
    if (!empty($permission)) {
      return user_access($permission);
    }
    return TRUE;
  }
  return FALSE;
}

/**
 * Returns the JS of the service worker.
 *
 * @return mixed
 */
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;
}

/**
 * Generate the for the manifest file.
 *
 * @return array
 */
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',
    // Default to "Pale Gray" from the Drupal Brand Palette
    // @see https://www.drupal.org/about/media-kit/logos
    'background_color' => variable_get('pwa_background_color', '#F6F6F2'),
    // Default to "Light blue" from the Drupal Brand Palette
    // @see https://www.drupal.org/about/media-kit/logos
    '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'),
    // Custom icons have to be defined in hook_pwa_manifest_alter().
    //
    // @see https://www.drupal.org/project/pwa/issues/2983031
    // @see pwa.api.php
    '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',
      ],
      // Used for Apple-related icons.
      [
        'src' => file_create_url($path . '/assets/drupal-black.svg'),
        'type' => 'image/svg+xml',
        'sizes' => '16x16',
        'purpose' => 'monochrome',
      ],
    ],
  ];
  drupal_alter('pwa_manifest', $manifest);
  return $manifest;
}

/**
 * Take the serviceworker template file and replace all the variables needed.
 *
 * @return string
 */
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');

  // Look up module release from package info.
  $pwa_module_version = pwa_version_assets('pwa');

  // Set up placeholders.
  $replace = [];

  // Data for the SW scripts
  $drupalPWASettings = [
    'version' => $pwa_module_version . '-v' . variable_get('pwa_sw_cache_version', 1),
    'debug' => (bool) variable_get('pwa_sw_debug', FALSE),
    'cache' => [
      // Never include these URLs in the SW cache.
      'exclude' => array_merge(_pwa_config_value_split('pwa_sw_cache_exclude', PWA_SW_CACHE_EXCLUDE), [
        '^/pwa/.*',
      ]),
      // Precached URLs. Add URLs using the 'Service Worker' tab of the Drupal UI.
      'precache' => [
        'page' => array_merge($precache_page, [
          variable_get('pwa_start_url', '/?source=pwa'),
          $offline_url,
        ]),
        // Cached assets. These are extracted using internal HTTP requests during Drupal
        // cache clears and this list will be hardcoded in the resultant SW file.
        'asset' => array_merge($precache_asset, [
          $offline_image,
        ]),
      ],
      // Strategies for caching specific patterns.
      'patterns' => [
        'page' => _pwa_config_value_explode('pwa_sw_patterns_page', ''),
        // Not used currently.
        'asset' => _pwa_config_value_explode('pwa_sw_patterns_asset', ''),
      ],
      'assets' => variable_get('pwa_sw_asset_config', PWA_SW_ASSETS),
      'offline' => [
        // When no connection is available, show this URL instead of the content that
        // should be available at the URL. This URL is never shown in the browser.
        'page' => $offline_url,
        // When an image hasn't been cached, we use this fallback image instead.
        'image' => $offline_image,
      ],
    ],
  ];

  // @todo introduce a plugin system to deal with additional features to the serviceworker.
  if (variable_get('pwa_sw_phonehome', TRUE)) {
    $drupalPWASettings['phonehome'] = [
      // Idle time in seconds afterwhich we should check if the user has access
      // to the serviceworker.
      'idle' => 10 * 60,
      // Number of page requests to wait until we check user access to the
      // serviceworker.
      'count' => 10,
    ];
  }
  drupal_alter('pwa_serviceworker_data', $drupalPWASettings);

  // Pretty print the json to make it easier to see what's in the SW.
  $replace['{/*drupalPWASettings*/}'] = json_encode($drupalPWASettings, JSON_PRETTY_PRINT | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);

  // List of scripts to import to build the serviceworker.
  $workbox_url = variable_get('pwa_workbox_url');
  $scripts = [
    'utils' => $path . '/js/serviceworker/utils.js',
    //'lifecycle' => $path . '/js/serviceworker/lifecycle.js',

    // @todo introduce a plugin system to deal with additional features to the serviceworker.
    '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);

  // Fill placeholders and return final file.
  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;
}

/**
 * Implements hook_html_head_alter().
 *
 * Make sure a viewport meta exists. Necessary for Lighthouse 100%.
 */
function pwa_html_head_alter(&$head_elements) {
  $has_viewport_meta = FALSE;

  // Check if the viewport meta already exists, add it if it doesn't.
  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,
    ];
  }

  // Add Apple related meta tags, do it in the alter to avoid adding the link
  // elements at the top of the page.
  if (!variable_get('pwa_apple_meta_enable', TRUE)) {
    return;
  }
  _pwa_apple_html_head_alter($head_elements);
}

/**
 * Implements hook_preprocess_html().
 *
 * Add the meta tags necessary for the manifest. Serviceworker script is
 * managed with a block.
 */
function pwa_preprocess_html(&$variables) {
  if (_pwa_access_check() && variable_get('pwa_sw_everywhere', FALSE)) {

    // Can't use #attached in this preprocess, call library functions directly.
    $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');

  // Add manifest.json to HTML
  drupal_add_html_head([
    '#tag' => 'link',
    '#attributes' => [
      'rel' => 'manifest',
      'href' => '/manifest.webmanifest?v=' . variable_get('pwa_sw_cache_version', 1),
      // Make sure that logged in users query the manifest as logged in users
      // @see https://web.dev/add-manifest/#link-manifest
      'crossorigin' => user_is_logged_in() ? 'use-credentials' : '',
    ],
    '#weight' => 10,
  ], 'manifest');

  // Add backup <meta> tag for branding colors. It should always match the
  // variable for the manifest.
  drupal_add_html_head([
    '#tag' => 'meta',
    '#attributes' => [
      'name' => 'theme-color',
      'content' => $manifest['theme_color'],
    ],
    '#weight' => 10,
  ], 'theme_color');

  // Add an icon for Apple devices. Necessary for Lighthouse 100%.
  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');
  }
}

/**
 * Implements hook_modernizr_info().
 */
function pwa_modernizr_info() {
  return [
    'serviceworker',
  ];
}

/**
 * Implements hook_user_logout().
 *
 * When users log out, clear all cookies, serviceworker.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data
 */
function pwa_user_logout($account) {
  drupal_add_http_header('Clear-Site-Data', '"storage", "cookies"');
}

/**
 * Implements hook_user_login().
 *
 * Clear all serviceworker/localstorage on login to make sure what gets
 * precached is up to date. By default SW is not enabled for authenticated
 * users.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data
 */
function pwa_user_login(&$edit, $account) {
  drupal_add_http_header('Clear-Site-Data', '"storage"');
}

/**
 * Menu callback to clear serviceworker and storage.
 */
function pwa_serviceworker_clear() {
  drupal_add_http_header('Clear-Site-Data', '"storage"');
  drupal_goto('<front>');
}

/**
 * Handle access permission returns.
 *
 * @param $page_callback_result
 *
 * @return bool
 */
function _pwa_check_page_callback_result($page_callback_result) {

  // Menu status constants are integers; page content is a string or array.
  if (is_int($page_callback_result)) {
    switch ($page_callback_result) {
      case MENU_ACCESS_DENIED:

        // Print a 403 page. This will make the Phone home check
        // UNINSTALL the serviceworker.
        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:

        // Print a 404 page. This will make the Phone home check
        // UNINSTALL the serviceworker.
        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:

        // Print a 503 page. This is a temporary error, the serviceworker will
        // NOT be uninstalled.
        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;
}

/**
 * Deliver a json response, don't involve the theme layer for a 'quick' check.
 *
 * @param $page_callback_result
 */
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();
}

/**
 * Deliver the JS for the service worker.
 *
 * Adds a Service-Worker-Allowed header so that a file served from
 * '/pwa/serviceworker/js' can have a scope of '/'.
 */
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());

    // Allow caching of serviceworker file for a week.
    drupal_add_http_header('Cache-Control', 'public, max-age=604800');
    print $page_callback_result;
  }
}

/**
 * Set the correct mime type for manifest file.
 */
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"');

    // Allow caching of manifest file for a week.
    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);
  }
}

/**
 * Implements hook_block_info().
 */
function pwa_block_info() {
  $blocks = [];
  $blocks['pwa_register'] = [
    'info' => t('PWA register serviceworker'),
    'weight' => 100,
    // Add the block to enable registration of the serviceworker by default.
    'status' => 1,
    'region' => 'footer',
    'visibility' => BLOCK_VISIBILITY_NOTLISTED,
    'pages' => variable_get('pwa_serviceworker_cache_exclude', PWA_SW_CACHE_EXCLUDE),
  ];
  return $blocks;
}

/**
 * Implements hook_block_view().
 */
function pwa_block_view($delta = '') {
  $block = [];
  if ($delta === 'pwa_register') {

    // There is no content, only attach libraries and settings.
    $block['content'] = [
      // Never install serviceworker for uid 1.
      '#access' => _pwa_access_check(),
      '#attached' => [
        'library' => [
          [
            'pwa',
            'register',
          ],
        ],
        'js' => [
          [
            'data' => [
              'pwa' => [
                'path' => url('pwa/serviceworker/js'),
              ],
            ],
            'type' => 'setting',
          ],
        ],
      ],
    ];
  }
  return $block;
}

/**
 * Implements hook_block_configure().
 *
 * This hook declares configuration options for blocks provided by this module.
 */
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;
}

Functions

Namesort descending Description
pwa_block_configure Implements hook_block_configure().
pwa_block_info Implements hook_block_info().
pwa_block_view Implements hook_block_view().
pwa_deliver_json Deliver a json response, don't involve the theme layer for a 'quick' check.
pwa_deliver_manifest_file Set the correct mime type for manifest file.
pwa_deliver_serviceworker_file Deliver the JS for the service worker.
pwa_file_data Returns the JS of the service worker.
pwa_html_head_alter Implements hook_html_head_alter().
pwa_library Implements hook_library().
pwa_menu Implements hook_menu().
pwa_modernizr_info Implements hook_modernizr_info().
pwa_permission Implements hook_permission().
pwa_preprocess_html Implements hook_preprocess_html().
pwa_serviceworker_clear Menu callback to clear serviceworker and storage.
pwa_user_login Implements hook_user_login().
pwa_user_logout Implements hook_user_logout().
pwa_version_assets Return a cache busting parameter for assets.
_pwa_access_check Custom access function to force the exclusion of the super admin for all serviceworker related things.
_pwa_check_page_callback_result Handle access permission returns.
_pwa_config_value_explode
_pwa_config_value_split
_pwa_manifest_file Generate the for the manifest file.
_pwa_serviceworker_file Take the serviceworker template file and replace all the variables needed.

Constants