301 lines
10 KiB
PHP
301 lines
10 KiB
PHP
<?php
|
|
|
|
/**
|
|
* "Converts" Roundcube into a Progressive Web Application
|
|
*
|
|
* @author Aleksander Machniak <machniak@kolabsys.com>
|
|
* @author Christian Mollekopf <mollekopf@kolabsys.com>
|
|
*
|
|
* Copyright (C) 2019, Kolab Systems AG <contact@kolabsys.com>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
class pwa extends rcube_plugin
|
|
{
|
|
public $noajax = true;
|
|
public $noframe = true;
|
|
|
|
/** @var string $version Plugin version */
|
|
public static $version = '0.1';
|
|
|
|
/** @var array $config Plugin config (from manifest.json) */
|
|
private static $config;
|
|
|
|
|
|
/**
|
|
* Plugin initialization
|
|
*/
|
|
function init()
|
|
{
|
|
$this->add_hook('template_object_links', array($this, 'template_object_links'));
|
|
$this->add_hook('template_object_meta', array($this, 'template_object_meta'));
|
|
|
|
$this->include_script('js/pwa.js');
|
|
}
|
|
|
|
/**
|
|
* Adds <link> elements to the HTML output
|
|
*/
|
|
public function template_object_links($args)
|
|
{
|
|
$rcube = rcube::get_instance();
|
|
$config = $this->get_config();
|
|
$content = '';
|
|
$links = array(
|
|
array(
|
|
'rel' => 'manifest',
|
|
'href' => '?PWA=manifest.json', //$this->urlbase . 'assets/manifest.json',
|
|
),
|
|
array(
|
|
'rel' => 'apple-touch-icon',
|
|
'sizes' => '180x180',
|
|
'href' => 'apple-touch-icon.png'
|
|
),
|
|
array(
|
|
'rel' => 'icon',
|
|
'type' => 'image/png',
|
|
'sizes' => '32x32',
|
|
'href' => 'favicon-32x32.png'
|
|
),
|
|
array(
|
|
'rel' => 'icon',
|
|
'type' => 'image/png',
|
|
'sizes' => '16x16',
|
|
'href' => 'favicon-16x16.png'
|
|
),
|
|
array(
|
|
'rel' => 'mask-icon',
|
|
'href' => 'safari-pinned-tab.svg',
|
|
'color' => $config['pinned_tab_color'] ?: $config['theme_color'],
|
|
),
|
|
);
|
|
|
|
// Check if the skin contains /pwa directory
|
|
$root_url = $rcube->find_asset('skins/' . $config['skin'] . '/pwa') ?: ($this->urlbase . 'assets');
|
|
|
|
foreach ($links as $link) {
|
|
if ($link['href'][0] != '?') {
|
|
$link['href'] = $root_url . '/' . $link['href'];
|
|
}
|
|
|
|
$content .= html::tag('link', $link) . "\n";
|
|
}
|
|
|
|
// replace favicon.ico
|
|
$icon = $root_url . '/favicon.ico';
|
|
$args['content'] = preg_replace('/(<link rel="shortcut icon" href=")([^"]+)/', '\\1' . $icon, $args['content']);
|
|
|
|
$args['content'] .= $content;
|
|
|
|
return $args;
|
|
}
|
|
|
|
/**
|
|
* Adds <meta> elements to the HTML output
|
|
*/
|
|
public function template_object_meta($args)
|
|
{
|
|
$config = $this->get_config();
|
|
$meta_content = '';
|
|
$meta_list = array(
|
|
'apple-mobile-web-app-title' => 'name',
|
|
'application-name' => 'name',
|
|
'msapplication-TileColor' => 'tile_color',
|
|
// todo: theme-color meta is already added by the skin, overwrite?
|
|
'theme-color' => 'theme_color',
|
|
);
|
|
|
|
foreach ($meta_list as $name => $opt_name) {
|
|
if ($content = $config[$opt_name]) {
|
|
$meta_content .= html::tag('meta', array('name' => $name, 'content' => $content)) . "\n";
|
|
}
|
|
}
|
|
|
|
$args['content'] .= $meta_content;
|
|
|
|
return $args;
|
|
}
|
|
|
|
/**
|
|
* Hijack HTTP requests to plugin assets e.g. service worker
|
|
*/
|
|
public static function http_request()
|
|
{
|
|
// We register service worker file from location specified
|
|
// as ?PWA=sw.js. This way we don't need to put it in Roundcube root
|
|
// and we can set some javascript variables like cache version, etc.
|
|
if ($_GET['PWA'] === 'sw.js') {
|
|
$rcube = rcube::get_instance();
|
|
$rcube->task = 'pwa';
|
|
$rcube->action = 'sw.js';
|
|
if ($file = $rcube->find_asset('plugins/pwa/js/sw.js')) {
|
|
// TODO: use caching headers?
|
|
header('Content-Type: application/javascript');
|
|
|
|
// TODO: What assets do we want to cache?
|
|
// TODO: assets_dir support
|
|
$assets = array(
|
|
// 'plugins/pwa/assets/manifest.json',
|
|
);
|
|
|
|
echo "var cacheName = 'v" . self::$version . "';\n";
|
|
echo "var assetsToCache = " . json_encode($assets) . "\n";
|
|
|
|
readfile($file);
|
|
exit;
|
|
}
|
|
|
|
header('HTTP/1.0 404 PWA plugin error');
|
|
exit;
|
|
}
|
|
|
|
// We genarate manifest.json file from skin/plugin config
|
|
if ($_GET['PWA'] === 'manifest.json') {
|
|
$rcube = rcube::get_instance();
|
|
$rcube->task = 'pwa';
|
|
$rcube->action = 'manifest.json';
|
|
|
|
// Read skin/plugin config
|
|
$config = self::get_config();
|
|
|
|
// HTTP scope
|
|
$scope = preg_replace('|/*\?.*$|', '', $_SERVER['REQUEST_URI']);
|
|
$scope = strlen($scope) ? $scope : '';
|
|
|
|
// Check if the skin contains /pwa directory
|
|
$root_url = $rcube->find_asset('skins/' . $config['skin'] . '/pwa') ?: ('plugins/pwa/assets');
|
|
|
|
// Manifest defaults
|
|
$defaults = array(
|
|
'name' => null,
|
|
'short_name' => $config['name'],
|
|
'description' => 'Free and Open Source Webmail',
|
|
'lang' => 'en-US',
|
|
'theme_color' => null,
|
|
'background_color' => null,
|
|
'pinned_tab_color' => null,
|
|
'icons' => array(
|
|
array(
|
|
'src' => $root_url . '/android-chrome-192x192.png',
|
|
'sizes' => '192x192',
|
|
'type' => 'image/png',
|
|
),
|
|
array(
|
|
'src' => $root_url . '/android-chrome-512x512.png',
|
|
'sizes' => '512x512',
|
|
'type' => 'image/png',
|
|
),
|
|
),
|
|
);
|
|
|
|
$manifest = array(
|
|
'scope' => $scope,
|
|
'start_url' => '?PWAMODE=1',
|
|
'display' => 'standalone',
|
|
/*
|
|
'permissions' => array(
|
|
'desktop-notification' => array(
|
|
'description' => "Needed for notifying you of any changes to your account."
|
|
),
|
|
),
|
|
*/
|
|
);
|
|
|
|
// Build manifest data from config and defaults
|
|
foreach ($defaults as $name => $value) {
|
|
if (isset($config[$name])) {
|
|
$value = $config[$name];
|
|
}
|
|
|
|
$manifest[$name] = $value;
|
|
}
|
|
|
|
// Send manifest.json to the browser
|
|
// TODO: use caching headers?
|
|
header('Content-Type: application/json');
|
|
echo rcube_output::json_serialize($manifest, (bool) $rcube->config->get('devel_mode'));
|
|
exit;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load plugin and skin configuration
|
|
*
|
|
* @return array Key-value configuration
|
|
*/
|
|
private static function get_config()
|
|
{
|
|
if (is_array(self::$config)) {
|
|
return self::$config;
|
|
}
|
|
|
|
$rcube = rcube::get_instance();
|
|
$config = array();
|
|
self::$config = array();
|
|
$defaults = array(
|
|
'tile_color' => '#2d89ef',
|
|
'theme_color' => '#f4f4f4',
|
|
'pinned_tab_color' => '#37beff',
|
|
'background_color' => '#ffffff',
|
|
'skin' => $rcube->config->get('skin') ?: 'elastic',
|
|
'name' => $rcube->config->get('product_name') ?: 'Roundcube',
|
|
);
|
|
|
|
// Load plugin config into $config var
|
|
$fpath = __DIR__ . '/config.inc.php';
|
|
if (is_file($fpath) && is_readable($fpath)) {
|
|
ob_start();
|
|
include($fpath);
|
|
ob_end_clean();
|
|
}
|
|
|
|
// Load skin config
|
|
$meta = @file_get_contents(RCUBE_INSTALL_PATH . '/skins/' . $defaults['skin'] . '/meta.json');
|
|
$meta = @json_decode($meta, true);
|
|
|
|
if ($meta && $meta['extends']) {
|
|
// Merge with parent skin config
|
|
$root_meta = @file_get_contents(RCUBE_INSTALL_PATH . '/skins/' . $meta['extends'] . '/meta.json');
|
|
$root_meta = @json_decode($meta, true);
|
|
|
|
if ($root_meta && !empty($root_meta['config'])) {
|
|
$meta['config'] = array_merge((array) $root_meta['config'], (array) $meta['config']);
|
|
}
|
|
}
|
|
|
|
foreach ((array) $meta['config'] as $name => $value) {
|
|
if (strpos($name, 'pwa_') === 0 && !isset($config[$name])) {
|
|
$config[$name] = $value;
|
|
}
|
|
}
|
|
|
|
foreach ($config as $name => $value) {
|
|
$name = preg_replace('/^pwa_/', '', $name);
|
|
if ($value !== null) {
|
|
self::$config[$name] = $value;
|
|
}
|
|
}
|
|
|
|
foreach ($defaults as $name => $value) {
|
|
if (!array_key_exists($name, self::$config)) {
|
|
self::$config[$name] = $value;
|
|
}
|
|
}
|
|
|
|
return self::$config;
|
|
}
|
|
}
|
|
|
|
// Hijack HTTP requests to special plugin assets e.g. sw.js, manifest.json
|
|
pwa::http_request();
|