diff --git a/acp/wpn_acp_info.php b/acp/wpn_acp_info.php index cb17476..03335bd 100644 --- a/acp/wpn_acp_info.php +++ b/acp/wpn_acp_info.php @@ -23,6 +23,11 @@ public function module() 'auth' => 'ext_phpbb/webpushnotifications && acl_a_server', 'cat' => ['ACP_CLIENT_COMMUNICATION'] ], + 'pwa' => [ + 'title' => 'ACP_WEBPUSH_PWA_SETTINGS', + 'auth' => 'ext_phpbb/webpushnotifications && acl_a_board', + 'cat' => ['ACP_CLIENT_COMMUNICATION'] + ], ], ]; } diff --git a/acp/wpn_acp_module.php b/acp/wpn_acp_module.php index 4015c4b..30c3e5f 100644 --- a/acp/wpn_acp_module.php +++ b/acp/wpn_acp_module.php @@ -17,6 +17,7 @@ use phpbb\symfony_request; use phpbb\template\template; use phpbb\user; +use phpbb\webpushnotifications\ext; class wpn_acp_module { @@ -24,15 +25,24 @@ class wpn_acp_module public $tpl_name; public $u_action; + /** @var \phpbb\cache\service */ + protected $cache; + /** @var config $config */ protected $config; + /** @var \phpbb\db\driver\driver_interface */ + protected $db; + /** @var language $lang */ protected $lang; /** @var log $log */ protected $log; + /** @var \FastImageSize\FastImageSize $imagesize */ + protected $imagesize; + /** @var request $request */ protected $request; @@ -45,6 +55,9 @@ class wpn_acp_module /** @var user $user */ protected $user; + /** @var string */ + protected $root_path; + /** @var array $errors */ protected $errors = []; @@ -62,13 +75,17 @@ public function main($id, $mode) { global $phpbb_container; + $this->cache = $phpbb_container->get('cache'); $this->config = $phpbb_container->get('config'); + $this->db = $phpbb_container->get('dbal.conn'); + $this->imagesize = $phpbb_container->get('upload_imagesize'); $this->lang = $phpbb_container->get('language'); $this->log = $phpbb_container->get('log'); $this->request = $phpbb_container->get('request'); $this->symfony_request = $phpbb_container->get('symfony_request'); $this->template = $phpbb_container->get('template'); $this->user = $phpbb_container->get('user'); + $this->root_path = $phpbb_container->getParameter('core.root_path'); $form_key = 'phpbb/webpushnotifications'; add_form_key($form_key); @@ -95,6 +112,26 @@ public function main($id, $mode) $this->display_settings(); } + else if ($mode === 'pwa') + { + $this->tpl_name = 'wpn_acp_pwa'; + + $this->lang->add_lang('webpushnotifications_module_acp', 'phpbb/webpushnotifications'); + + $this->page_title = $this->lang->lang('ACP_WEBPUSH_PWA_SETTINGS'); + + if ($this->request->is_set_post('submit')) + { + if (!check_form_key($form_key)) + { + trigger_error($this->lang->lang('FORM_INVALID'), E_USER_WARNING); + } + + $this->save_pwa_settings(); + } + + $this->display_pwa_settings(); + } } /** @@ -164,6 +201,218 @@ public function save_settings() trigger_error($this->lang->lang('CONFIG_UPDATED') . adm_back_link($this->u_action), E_USER_NOTICE); } + /** + * Add PWA settings template vars to the form + */ + public function display_pwa_settings() + { + $this->template->assign_vars([ + 'S_PWA_SHOW_INSTALL_BANNER' => (bool) $this->config['pwa_show_install_banner'], + 'PWA_SHORT_NAME' => $this->config['pwa_short_name'], + 'PWA_ICON_SMALL' => $this->config['pwa_icon_small'], + 'PWA_ICON_LARGE' => $this->config['pwa_icon_large'], + 'STYLES' => $this->get_styles(), + 'U_ACTION' => $this->u_action, + ]); + + $this->display_errors(); + } + + /** + * Save PWA settings data to the database + * + * @return void + */ + public function save_pwa_settings() + { + $config_array = $this->request->variable('config', ['' => ''], true); + + $config_array['pwa_short_name'] = $config_array['pwa_short_name'] ?? ''; + $config_array['pwa_icon_small'] = $config_array['pwa_icon_small'] ?? ''; + $config_array['pwa_icon_large'] = $config_array['pwa_icon_large'] ?? ''; + + $this->validate_pwa_short_name($config_array['pwa_short_name']); + $this->validate_pwa_icons($config_array['pwa_icon_small'], $config_array['pwa_icon_large']); + + $styles = $this->get_styles(); + $updates = []; + foreach ($styles as $row) + { + $style_id = $row['style_id']; + $pwa_bg_color = $this->request->variable('pwa_bg_color_' . $style_id, ''); + $pwa_theme_color = $this->request->variable('pwa_theme_color_' . $style_id, ''); + + $updates[$style_id] = [ + 'pwa_bg_color' => $this->validate_hex_color($pwa_bg_color) ? $pwa_bg_color : $row['pwa_bg_color'], + 'pwa_theme_color' => $this->validate_hex_color($pwa_theme_color) ? $pwa_theme_color : $row['pwa_theme_color'], + ]; + } + + if ($this->display_errors()) + { + return; + } + + $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_CONFIG_WEBPUSH'); + + // Ensure 4-byte emoji can be stored correctly + $config_array['pwa_short_name'] = utf8_encode_ucr($config_array['pwa_short_name']); + + foreach ([ + 'pwa_short_name', + 'pwa_icon_small', + 'pwa_icon_large', + 'pwa_show_install_banner', + ] as $config_name) + { + $this->config->set($config_name, $config_array[$config_name] ?? 0); + } + + $this->set_styles($updates); + + trigger_error($this->lang->lang('CONFIG_UPDATED') . adm_back_link($this->u_action), E_USER_NOTICE); + } + + /** + * Validate PWA short site name + */ + protected function validate_pwa_short_name(string $short_name): void + { + if ($short_name === '') + { + return; + } + + $short_name = ext::decode_entities($short_name, ENT_QUOTES); + if (utf8_strlen($short_name) > 12) + { + $this->errors[] = $this->lang->lang('PWA_SHORT_NAME_INVALID'); + } + } + + /** + * Validate PWA icon filenames and dimensions + */ + protected function validate_pwa_icons(string $small_icon, string $large_icon): void + { + if ($small_icon === '' && $large_icon === '') + { + return; + } + + if ($small_icon === '') + { + $this->errors[] = $this->lang->lang('PWA_ICON_NOT_PROVIDED', $this->lang->lang('PWA_ICON_SMALL')); + return; + } + + if ($large_icon === '') + { + $this->errors[] = $this->lang->lang('PWA_ICON_NOT_PROVIDED', $this->lang->lang('PWA_ICON_LARGE')); + return; + } + + $this->validate_pwa_icon($small_icon, 192); + $this->validate_pwa_icon($large_icon, 512); + } + + /** + * Validate one PWA icon file + */ + protected function validate_pwa_icon(string $filename, int $size): void + { + if (basename($filename) !== $filename) + { + $this->errors[] = $this->lang->lang('PWA_ICON_INVALID', $filename); + return; + } + + $image = $this->root_path . ext::PWA_ICON_DIR . '/' . $filename; + $image_info = $this->imagesize->getImageSize($image); + if ($image_info === false) + { + $this->errors[] = $this->lang->lang('PWA_ICON_INVALID', $filename); + return; + } + + if ($image_info['width'] !== $size || $image_info['height'] !== $size) + { + $this->errors[] = $this->lang->lang('PWA_ICON_SIZE_INVALID', $filename); + } + + if ($image_info['type'] !== IMAGETYPE_PNG) + { + $this->errors[] = $this->lang->lang('PWA_ICON_MIME_INVALID', $filename); + } + } + + /** + * Validate HTML color hex codes + */ + protected function validate_hex_color(string $code): bool + { + $code = trim($code); + + if ($code === '') + { + return true; + } + + $test = (bool) preg_match('/^#([0-9A-F]{3}){1,2}$/i', $code); + + if ($test === false) + { + $this->errors[] = $this->lang->lang('PWA_INVALID_COLOUR', $code); + } + + return $test; + } + + /** + * Get style data from the styles table + * + * @return array Style data + */ + protected function get_styles(): array + { + $sql = 'SELECT style_id, style_name, pwa_bg_color, pwa_theme_color + FROM ' . STYLES_TABLE . ' + WHERE style_active = 1 + ORDER BY style_name'; + $result = $this->db->sql_query($sql, 3600); + + $rows = $this->db->sql_fetchrowset($result); + $this->db->sql_freeresult($result); + + return $rows; + } + + /** + * Set style data in the styles table + * + * @param array $rows Array of style table data to update; style_id is key + * @return void + */ + protected function set_styles(array $rows): void + { + if (!empty($rows)) + { + $this->db->sql_transaction('begin'); + + foreach ($rows as $style_id => $row) + { + $sql = 'UPDATE ' . STYLES_TABLE . ' + SET ' . $this->db->sql_build_array('UPDATE', $row) . ' + WHERE style_id = ' . (int) $style_id; + $this->db->sql_query($sql); + } + + $this->db->sql_transaction('commit'); + + $this->cache->destroy('sql', STYLES_TABLE); + } + } + /** * Display any errors * diff --git a/adm/style/event/acp_overall_footer_after.html b/adm/style/event/acp_overall_footer_after.html deleted file mode 100644 index 09748c1..0000000 --- a/adm/style/event/acp_overall_footer_after.html +++ /dev/null @@ -1,18 +0,0 @@ -{% if S_PWA_OPTIONS %} - -{% endif %} diff --git a/adm/style/pwa_acp.css b/adm/style/pwa_acp.css new file mode 100644 index 0000000..961f0ef --- /dev/null +++ b/adm/style/pwa_acp.css @@ -0,0 +1,29 @@ +input[type="color"] { + background-color: transparent; + border: solid 1px #d3d3d3; + border-radius: 50%; + width: 24px; + height: 24px; + padding: 2px; + cursor: pointer; + -webkit-appearance: none; +} + +input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; +} + +input[type="color"]::-webkit-color-swatch { + border: 0; + border-radius: 50%; +} + +input[type="color"]::-moz-color-swatch { + border: 0; + border-radius: 50%; +} + +.color-pickers { + display: inline-block; + width: 120px; +} diff --git a/adm/style/pwa_acp.js b/adm/style/pwa_acp.js new file mode 100644 index 0000000..8705500 --- /dev/null +++ b/adm/style/pwa_acp.js @@ -0,0 +1,38 @@ +document.addEventListener('DOMContentLoaded', () => { + 'use strict'; + + const HEX_REGEX = /^#([A-Fa-f0-9]{6})$/; + + const colorPickers = document.querySelectorAll('input[type="color"]'); + + colorPickers.forEach(colorPicker => { + const colorText = colorPicker.previousElementSibling; + + if (!colorText || colorText.type !== 'text') { + return; + } + + const syncColors = (source, target) => { + const value = source.value.trim(); + target.value = HEX_REGEX.test(value) ? value : colorText.placeholder; + }; + + const handleInput = ({ target }) => { + if (target === colorPicker) { + colorText.value = target.value; + } else { + syncColors(colorText, colorPicker); + } + }; + + colorPicker.addEventListener('input', handleInput); + colorText.addEventListener('input', handleInput); + colorText.addEventListener('blur', () => { + if (!colorText.value.trim()) { + colorPicker.value = colorText.placeholder; + } + }); + + syncColors(colorText, colorPicker); + }); +}); diff --git a/adm/style/wpn_acp_pwa.html b/adm/style/wpn_acp_pwa.html new file mode 100644 index 0000000..169a5d4 --- /dev/null +++ b/adm/style/wpn_acp_pwa.html @@ -0,0 +1,73 @@ +{% include 'overall_header.html' %} +{% INCLUDECSS '@phpbb_webpushnotifications/pwa_acp.css' %} +{% INCLUDEJS '@phpbb_webpushnotifications/pwa_acp.js' %} + +

{{ lang('ACP_WEBPUSH_PWA_SETTINGS') }}

+ +

{{ lang('ACP_PWA_SETTINGS_EXPLAIN') }}

+ +{% if S_ERROR %} +
+

{{ lang('WARNING') }}

+

{{ ERROR_MSG }}

+
+{% endif %} + +
+
+ {{ lang('GENERAL_SETTINGS') }} +
+

{{ lang('PWA_SHORT_NAME_EXPLAIN') }}
+
+
+
+

{{ lang('PWA_ICON_SMALL_EXPLAIN') }}
+
{{ constant('phpbb\\webpushnotifications\\ext::PWA_ICON_DIR') }}/
+
+
+

{{ lang('PWA_ICON_LARGE_EXPLAIN') }}
+
{{ constant('phpbb\\webpushnotifications\\ext::PWA_ICON_DIR') }}/
+
+
+

{{ lang('PWA_SHOW_INSTALL_BANNER_EXPLAIN') }}
+
+
+
+
+ +

{{ lang('PWA_THEME_COLOURS') }}

+

{{ lang('PWA_THEME_COLOURS_EXPLAIN') }}

+
+ {{ lang('PWA_COLOURS') }} +
+
+
+ {{ lang('PWA_THEME_COLOUR') }} + {{ lang('PWA_BACKGROUND_COLOUR') }} +
+
+ {% for style in STYLES %} +
+
+
+ + + + + + + + +
+
+ {% endfor %} +
+ +
+   + + {{ S_FORM_TOKEN }} +
+
+ +{% include 'overall_footer.html' %} diff --git a/config/services.yml b/config/services.yml index e21730d..bc3120c 100644 --- a/config/services.yml +++ b/config/services.yml @@ -14,13 +14,12 @@ services: arguments: - '@config' - '@controller.helper' - - '@upload_imagesize' - '@phpbb.wpn.form_helper' - '@language' + - '@request' - '@template' - '@user' - '@notification_manager' - - '%core.root_path%' tags: - { name: event.listener } diff --git a/controller/manifest.php b/controller/manifest.php index 968a84e..a1b71ba 100644 --- a/controller/manifest.php +++ b/controller/manifest.php @@ -61,6 +61,8 @@ public function handle(): JsonResponse 'orientation' => 'portrait', 'start_url' => $start_url, 'scope' => $scope, + 'theme_color' => !empty($this->user->style['pwa_theme_color']) ? $this->user->style['pwa_theme_color'] : ext::PWA_THEME_COLOR, + 'background_color' => !empty($this->user->style['pwa_bg_color']) ? $this->user->style['pwa_bg_color'] : ext::PWA_BG_COLOR, ]; if (!empty($this->config['pwa_icon_small']) && !empty($this->config['pwa_icon_large'])) @@ -80,6 +82,7 @@ public function handle(): JsonResponse } $response = new JsonResponse($manifest); + $response->headers->set('Content-Type', 'application/manifest+json'); $response->setPublic(); $response->setMaxAge(3600); $response->headers->addCacheControlDirective('must-revalidate', true); diff --git a/event/listener.php b/event/listener.php index 462a9ff..4e5ae34 100644 --- a/event/listener.php +++ b/event/listener.php @@ -10,11 +10,11 @@ namespace phpbb\webpushnotifications\event; -use FastImageSize\FastImageSize; use phpbb\config\config; use phpbb\controller\helper as controller_helper; use phpbb\language\language; use phpbb\notification\manager; +use phpbb\request\request_interface; use phpbb\template\template; use phpbb\user; use phpbb\webpushnotifications\ext; @@ -35,12 +35,12 @@ class listener implements EventSubscriberInterface /* @var form_helper */ protected $form_helper; - /** @var FastImageSize */ - protected $imagesize; - /* @var language */ protected $language; + /** @var request_interface */ + protected $request; + /* @var template */ protected $template; @@ -50,33 +50,28 @@ class listener implements EventSubscriberInterface /* @var manager */ protected $phpbb_notifications; - /** @var string */ - protected $root_path; - /** * Constructor * * @param config $config * @param controller_helper $controller_helper Controller helper object - * @param FastImageSize $imagesize * @param form_helper $form_helper Form helper object * @param language $language Language object + * @param request_interface $request * @param template $template Template object * @param user $user * @param manager $phpbb_notifications Notifications manager object - * @param string $root_path */ - public function __construct(config $config, controller_helper $controller_helper, FastImageSize $imagesize, form_helper $form_helper, language $language, template $template, user $user, manager $phpbb_notifications, $root_path) + public function __construct(config $config, controller_helper $controller_helper, form_helper $form_helper, language $language, request_interface $request, template $template, user $user, manager $phpbb_notifications) { $this->config = $config; $this->controller_helper = $controller_helper; - $this->imagesize = $imagesize; $this->form_helper = $form_helper; $this->language = $language; + $this->request = $request; $this->template = $template; $this->user = $user; $this->phpbb_notifications = $phpbb_notifications; - $this->root_path = $root_path; } public static function getSubscribedEvents() @@ -86,9 +81,6 @@ public static function getSubscribedEvents() 'core.page_header_after' => [['load_template_data'], ['pwa_manifest']], 'core.ucp_display_module_before' => 'load_language', 'core.acp_main_notice' => 'compatibility_notice', - 'core.acp_board_config_edit_add' => 'acp_pwa_options', - 'core.acp_board_config_emoji_enabled' => 'acp_pwa_allow_emoji', - 'core.validate_config_variable' => 'validate_pwa_options', 'core.help_manager_add_block_after' => 'wpn_faq', ]; } @@ -163,155 +155,12 @@ public function pwa_manifest() 'U_MANIFEST_URL' => $this->controller_helper->route('phpbb_webpushnotifications_manifest_controller'), 'U_TOUCH_ICON' => $this->config['pwa_icon_small'] ? ext::PWA_ICON_DIR . '/' . $this->config['pwa_icon_small'] : null, 'SHORT_SITE_NAME' => $this->config['pwa_short_name'] ?: $this->trim_shortname($this->config['sitename']), + 'PWA_THEME_COLOR' => !empty($this->user->style['pwa_theme_color']) ? $this->user->style['pwa_theme_color'] : ext::PWA_THEME_COLOR, + 'PWA_BG_COLOR' => !empty($this->user->style['pwa_bg_color']) ? $this->user->style['pwa_bg_color'] : ext::PWA_BG_COLOR, + 'S_PWA_SHOW_BANNER' => !empty($this->config['pwa_show_install_banner']) && $this->is_mobile_phone(), ]); } - /** - * Progressive web app options for the ACP - * - * @param \phpbb\event\data $event - * @return void - */ - public function acp_pwa_options($event) - { - if ($event['mode'] === 'settings' && array_key_exists('legend4', $event['display_vars']['vars'])) - { - $this->language->add_lang('webpushnotifications_common_acp', 'phpbb/webpushnotifications'); - - $my_config_vars = [ - 'legend_pwa_settings'=> 'PWA_SETTINGS', - 'pwa_short_name' => ['lang' => 'PWA_SHORT_NAME', 'validate' => 'pwa_options:string', 'type' => 'custom', 'function' => [$this, 'pwa_short_sitename'], 'explain' => true], - 'pwa_icon_small' => ['lang' => 'PWA_ICON_SMALL', 'validate' => 'pwa_options:icons', 'type' => 'custom', 'function' => [$this, 'pwa_icon_name'], 'explain' => true], - 'pwa_icon_large' => ['lang' => 'PWA_ICON_LARGE', 'validate' => 'pwa_options:icons', 'type' => 'custom', 'function' => [$this, 'pwa_icon_name'], 'explain' => true], - ]; - - $event->update_subarray('display_vars', 'vars', phpbb_insert_config_array($event['display_vars']['vars'], $my_config_vars, ['before' => 'legend4'])); - - $this->template->assign_var('S_PWA_OPTIONS', true); - } - } - - /** - * Allow PWA short name ACP field to accept emoji characters - * - * @param \phpbb\event\data $event - * @return void - */ - public function acp_pwa_allow_emoji($event) - { - if (in_array('pwa_short_name', $event['config_name_ary'], true)) - { - return; - } - - $config_name_ary = $event['config_name_ary']; - $config_name_ary[] = 'pwa_short_name'; - $event['config_name_ary'] = $config_name_ary; - } - - /** - * Return HTML for PWA icon name settings - * - * @param string $value Value of config - * @param string $key Name of config - * @return string - */ - public function pwa_icon_name($value, $key) - { - return ext::PWA_ICON_DIR . '/'; - } - - /** - * Return HTML for PWA short site name setting - * - * @param string $value Value of config - * @param string $key Name of config - * @return string - */ - public function pwa_short_sitename($value, $key) - { - $placeholder = $this->trim_shortname($this->config['sitename']); - - return ''; - } - - /** - * Validate PWA options - * - * @param \phpbb\event\data $event - * @return void - */ - public function validate_pwa_options($event) - { - $type = 0; - $mode = 1; - - $validator = explode(':', $event['config_definition']['validate']); - - if ($validator[$type] !== 'pwa_options') - { - return; - } - - switch ($validator[$mode]) - { - case 'string': - // Ignore validation if icon fields are empty - if (empty($event['cfg_array']['pwa_short_name'])) - { - return; - } - - $short_name = ext::decode_entities($event['cfg_array']['pwa_short_name'], ENT_QUOTES); - - // Do not allow strings longer than 12 characters - if (utf8_strlen($short_name) > 12) - { - $this->add_error($event, 'PWA_SHORT_NAME_INVALID'); - return; - } - break; - - case 'icons': - // Ignore validation if icon fields are empty - if (empty($event['cfg_array']['pwa_icon_small']) && empty($event['cfg_array']['pwa_icon_large'])) - { - return; - } - - $value = $event['cfg_array'][$event['config_name']]; - - // Don't allow empty values, if one icon is set, both must be set. - if (empty($value)) - { - $this->add_error($event, 'PWA_ICON_NOT_PROVIDED', $this->language->lang(strtoupper($event['config_name']))); - return; - } - - // Check if image is valid - $image = $this->root_path . ext::PWA_ICON_DIR . '/' . $value; - $image_info = $this->imagesize->getImageSize($image); - if ($image_info !== false) - { - if (($event['config_name'] === 'pwa_icon_small' && $image_info['width'] !== 192 && $image_info['height'] !== 192) || - ($event['config_name'] === 'pwa_icon_large' && $image_info['width'] !== 512 && $image_info['height'] !== 512)) - { - $this->add_error($event, 'PWA_ICON_SIZE_INVALID', $value); - } - - if ($image_info['type'] !== IMAGETYPE_PNG) - { - $this->add_error($event, 'PWA_ICON_MIME_INVALID', $value); - } - } - else - { - $this->add_error($event, 'PWA_ICON_INVALID', $value); - } - break; - } - } - /** * Add Web Push info to the phpBB FAQ * @@ -350,21 +199,6 @@ public function wpn_faq($event) } } - /** - * Add errors to the error array - * - * @param \phpbb\event\data $event - * @param string $error_key - * @param string $param - * @return void - */ - protected function add_error($event, $error_key, $param = null) - { - $error = $event['error']; - $error[] = $this->language->lang($error_key, $param); - $event['error'] = $error; - } - /** * Can notifications be used by the user? * @@ -389,4 +223,20 @@ protected function trim_shortname($name) $trimmed = utf8_substr($decoded, 0, 12); return utf8_htmlspecialchars($trimmed); } + + /** + * Lightweight phone detection for install-banner display + * + * @return bool + */ + protected function is_mobile_phone() + { + $user_agent = (string) $this->request->server('HTTP_USER_AGENT', ''); + if ($user_agent === '' || preg_match('/ipad|tablet|kindle|silk|playbook/i', $user_agent)) + { + return false; + } + + return (bool) preg_match('/mobile|iphone|ipod|android.*mobile|windows phone|blackberry|opera mini/i', $user_agent); + } } diff --git a/ext.php b/ext.php index 56f2872..e58af68 100644 --- a/ext.php +++ b/ext.php @@ -20,6 +20,12 @@ class ext extends \phpbb\extension\base */ public const PWA_ICON_DIR = 'images/site_icons'; + /** @var string PWA theme color */ + public const PWA_THEME_COLOR = '#000000'; + + /** @var string PWA background color */ + public const PWA_BG_COLOR = '#ffffff'; + /** * Require phpBB 3.3.12 due to new template and core events. */ diff --git a/language/en/common.php b/language/en/common.php index 36a6e24..99704aa 100644 --- a/language/en/common.php +++ b/language/en/common.php @@ -38,5 +38,9 @@ // $lang = array_merge($lang, [ + 'PWA_INSTALL_BUTTON' => 'Install app', + 'PWA_INSTALL_DISMISS' => 'Close', + 'PWA_INSTALL_SUBTITLE' => 'Add to home screen', + 'PWA_INSTALL_SUBTITLE_IOS' => 'Tap Share and then “Add to Home Screen” to install this app.', 'WEBPUSH_INVALID_ENDPOINT' => 'The push notification endpoint is not from a recognised push service.', ]); diff --git a/language/en/info_acp_webpushnotifications.php b/language/en/info_acp_webpushnotifications.php index 7e01c05..dfecca9 100644 --- a/language/en/info_acp_webpushnotifications.php +++ b/language/en/info_acp_webpushnotifications.php @@ -39,6 +39,7 @@ $lang = array_merge($lang, [ 'ACP_WEBPUSH_EXT_SETTINGS' => 'Web Push settings', + 'ACP_WEBPUSH_PWA_SETTINGS' => 'PWA settings', 'ACP_WEBPUSH_REMOVE_WARNING' => 'Web Push Notifications are now built into phpBB', 'ACP_WEBPUSH_REMOVE_NOTICE' => 'The extension “phpBB Browser Push Notifications” is no longer needed and should be uninstalled and removed.
All settings and user preferences associated with the extension will be migrated into phpBB’s built-in push notifications when you uninstall the extension.', 'LOG_CONFIG_WEBPUSH' => 'Altered Web Push settings', diff --git a/language/en/webpushnotifications_common_acp.php b/language/en/webpushnotifications_common_acp.php deleted file mode 100644 index 7cfb060..0000000 --- a/language/en/webpushnotifications_common_acp.php +++ /dev/null @@ -1,53 +0,0 @@ - - * @license GNU General Public License, version 2 (GPL-2.0) - * - */ - -/** - * DO NOT CHANGE - */ -if (!defined('IN_PHPBB')) -{ - exit; -} - -if (empty($lang) || !is_array($lang)) -{ - $lang = []; -} - -// DEVELOPERS PLEASE NOTE -// -// All language files should use UTF-8 as their encoding and the files must not contain a BOM. -// -// Placeholders can now contain order information, e.g. instead of -// 'Page %s of %s' you can (and should) write 'Page %1$s of %2$s', this allows -// translators to re-order the output of data while ensuring it remains correct -// -// You do not need this where single placeholders are used, e.g. 'Message %d' is fine -// equally where a string contains only two placeholders which are used to wrap text -// in a url you again do not need to specify an order e.g., 'Click %sHERE%s' is fine -// -// Some characters you may want to copy&paste: -// ’ » “ ” … -// - -$lang = array_merge($lang, [ - 'PWA_SETTINGS' => 'Progressive web application options', - 'PWA_SHORT_NAME' => 'Short site name', - 'PWA_SHORT_NAME_EXPLAIN' => 'Your site name in 12 characters or fewer, which may be used as a label for an icon on a mobile device’s home screen. (If this field is left empty, the first 12 characters of the Site name will be used.)', - 'PWA_SHORT_NAME_INVALID' => '“Short site name” exceeds the 12 character limit.', - 'PWA_ICON_SMALL' => 'Small mobile device icon', - 'PWA_ICON_SMALL_EXPLAIN' => 'File name of a 192px x 192px PNG image. This file must be uploaded to your board’s ' . \phpbb\webpushnotifications\ext::PWA_ICON_DIR . ' directory.', - 'PWA_ICON_LARGE' => 'Large mobile device icon', - 'PWA_ICON_LARGE_EXPLAIN' => 'File name of a 512px x 512px PNG image. This file must be uploaded to your board’s ' . \phpbb\webpushnotifications\ext::PWA_ICON_DIR . ' directory.', - 'PWA_ICON_SIZE_INVALID' => '“%s” does not have the correct image dimensions.', - 'PWA_ICON_MIME_INVALID' => '“%s” must be a PNG image file.', - 'PWA_ICON_INVALID' => '“%s” is not a valid image file or is missing from the expected location. Verify the file name and location are correct.', - 'PWA_ICON_NOT_PROVIDED' => '%s field must not be empty. All icon fields must contain an image.', -]); diff --git a/language/en/webpushnotifications_module_acp.php b/language/en/webpushnotifications_module_acp.php index 9d83cd0..3929a27 100644 --- a/language/en/webpushnotifications_module_acp.php +++ b/language/en/webpushnotifications_module_acp.php @@ -38,7 +38,8 @@ // $lang = array_merge($lang, [ - 'ACP_WEBPUSH_SETTINGS_EXPLAIN' => 'Here you can enable Web Push for board notifications. Web Push is a protocol for the real-time delivery of events to user agents, commonly referred to as push messages. It is compatible with the majority of modern browsers on both desktop and mobile devices. Users can opt to receive Web Push alerts in their browser by subscribing and enabling their preferred notifications in the UCP.

To enable push notifications on Apple mobile devices, your site needs to function as a Progressive Web Application (PWA). This requires users to add your site to their device’s home screen. For an improved user experience, you can find additional PWA settings under Board settings, where you can configure an optional app name and app icons for how your site will appear on users’ home screens.', + // Web push settings + 'ACP_WEBPUSH_SETTINGS_EXPLAIN' => 'Here you can enable Web Push for board notifications. Web Push is a protocol for the real-time delivery of events to user agents, commonly referred to as push messages. It is compatible with the majority of modern browsers on both desktop and mobile devices. Users can opt to receive Web Push alerts in their browser by subscribing and enabling their preferred notifications in the UCP.

To enable push notifications on Apple mobile devices, your site needs to function as a Progressive Web Application (PWA). This requires users to add your site to their device’s home screen. Configure app metadata and install behaviour under PWA settings.', 'WEBPUSH_ENABLE' => 'Enable Web Push', 'WEBPUSH_ENABLE_EXPLAIN' => 'Allow users to receive notifications in their browser or device via Web Push. To utilise Web Push, you must input or generate valid VAPID identification keys.', 'WEBPUSH_GENERATE_VAPID_KEYS' => 'Generate Identification keys', @@ -53,4 +54,26 @@ 'WEBPUSH_POPUP_PROMPT' => 'Show popup prompt for unsubscribed members', 'WEBPUSH_POPUP_PROMPT_EXPLAIN' => 'Display a popup message asking registered members if they want to receive push notifications. The popup will only appear to members who are not currently subscribed and have not previously denied.', 'WEBPUSH_INSECURE_SERVER_ERROR' => 'This board is not using a secure SSL/HTTPS protocol, which is required for enabling web push notifications. Alternatively, the server environment might be misconfigured. Ensure that the HTTPS and HEADER_CLIENT_PROTO server environment variables are correctly configured.', + + // PWA Settings + 'ACP_PWA_SETTINGS_EXPLAIN' => 'Here you can configure Progressive Web App behaviour, including app manifest metadata and install banner display.', + 'PWA_SHORT_NAME' => 'Short site name', + 'PWA_SHORT_NAME_EXPLAIN' => 'Your site name in 12 characters or fewer, which may be used as a label for an icon on a mobile device’s home screen. (If this field is left empty, the first 12 characters of the Site name will be used.)', + 'PWA_SHORT_NAME_INVALID' => '“Short site name” exceeds the 12 character limit.', + 'PWA_ICON_SMALL' => 'Small mobile device icon', + 'PWA_ICON_SMALL_EXPLAIN' => 'File name of a 192px x 192px PNG image. This file must be uploaded to your board’s ' . \phpbb\webpushnotifications\ext::PWA_ICON_DIR . ' directory.', + 'PWA_ICON_LARGE' => 'Large mobile device icon', + 'PWA_ICON_LARGE_EXPLAIN' => 'File name of a 512px x 512px PNG image. This file must be uploaded to your board’s ' . \phpbb\webpushnotifications\ext::PWA_ICON_DIR . ' directory.', + 'PWA_ICON_SIZE_INVALID' => '“%s” does not have the correct image dimensions.', + 'PWA_ICON_MIME_INVALID' => '“%s” must be a PNG image file.', + 'PWA_ICON_INVALID' => '“%s” is not a valid image file or is missing from the expected location. Verify the file name and location are correct.', + 'PWA_ICON_NOT_PROVIDED' => '%s field must not be empty. All icon fields must contain an image.', + 'PWA_THEME_COLOURS' => 'Theme & background colours (optional)', + 'PWA_THEME_COLOURS_EXPLAIN' => 'Used to specify the default colour for your web application’s user interface. Theme colours may be applied to various browser UI elements, such as the toolbar, address bar, and status bar. Background colours appears in the application window before your application’s stylesheets have loaded.', + 'PWA_COLOURS' => 'Colours', + 'PWA_THEME_COLOUR' => 'Theme colour', + 'PWA_BACKGROUND_COLOUR' => 'Background colour', + 'PWA_INVALID_COLOUR' => 'The colour code “%s” is not a valid hex code.', + 'PWA_SHOW_INSTALL_BANNER' => 'Show install banner', + 'PWA_SHOW_INSTALL_BANNER_EXPLAIN' => 'Display a mobile install prompt when the browser reports that your board can be installed as an app.', ]); diff --git a/language/ru/common.php b/language/ru/common.php index 8ea5c41..50188d7 100644 --- a/language/ru/common.php +++ b/language/ru/common.php @@ -38,5 +38,9 @@ // $lang = array_merge($lang, [ + 'PWA_INSTALL_BUTTON' => 'Install app', + 'PWA_INSTALL_DISMISS' => 'Close', + 'PWA_INSTALL_SUBTITLE' => 'Add to home screen', + 'PWA_INSTALL_SUBTITLE_IOS' => 'Tap Share and then “Add to Home Screen” to install this app.', 'WEBPUSH_INVALID_ENDPOINT' => 'Конечная точка push-уведомления не принадлежит известному сервису push-уведомлений.', ]); diff --git a/language/ru/info_acp_webpushnotifications.php b/language/ru/info_acp_webpushnotifications.php index b1c1564..8355db6 100644 --- a/language/ru/info_acp_webpushnotifications.php +++ b/language/ru/info_acp_webpushnotifications.php @@ -39,6 +39,7 @@ $lang = array_merge($lang, [ 'ACP_WEBPUSH_EXT_SETTINGS' => 'Браузерные уведомления', + 'ACP_WEBPUSH_PWA_SETTINGS' => 'PWA settings', 'ACP_WEBPUSH_REMOVE_WARNING' => 'Браузерные push—уведомления теперь встроены в phpBB', 'ACP_WEBPUSH_REMOVE_NOTICE' => 'Расширение «phpBB Browser Push Notifications» больше не требуется и должно быть удалено.
Все системные и пользовательские настройки, связанные с данным расширением, будут перенесены в соответствующие настройки браузерных push—уведомлений конференции автоматически при удалении данного расширения.', 'LOG_CONFIG_WEBPUSH' => 'Изменены настройки браузерных push—уведомлений', diff --git a/language/ru/webpushnotifications_common_acp.php b/language/ru/webpushnotifications_common_acp.php deleted file mode 100644 index 7107b4f..0000000 --- a/language/ru/webpushnotifications_common_acp.php +++ /dev/null @@ -1,53 +0,0 @@ - - * @license GNU General Public License, version 2 (GPL-2.0) - * - */ - -/** - * DO NOT CHANGE - */ -if (!defined('IN_PHPBB')) -{ - exit; -} - -if (empty($lang) || !is_array($lang)) -{ - $lang = []; -} - -// DEVELOPERS PLEASE NOTE -// -// All language files should use UTF-8 as their encoding and the files must not contain a BOM. -// -// Placeholders can now contain order information, e.g. instead of -// 'Page %s of %s' you can (and should) write 'Page %1$s of %2$s', this allows -// translators to re-order the output of data while ensuring it remains correct -// -// You do not need this where single placeholders are used, e.g. 'Message %d' is fine -// equally where a string contains only two placeholders which are used to wrap text -// in a url you again do not need to specify an order e.g., 'Click %sHERE%s' is fine -// -// Some characters you may want to copy&paste: -// ’ » “ ” … -// - -$lang = array_merge($lang, [ - 'PWA_SETTINGS' => 'Настройки прогрессивного веб—приложения (PWA)', - 'PWA_SHORT_NAME' => 'Краткое имя сайта', - 'PWA_SHORT_NAME_EXPLAIN' => 'Краткое имя сайта длиной не более 12 символов, которое будет использовано в качестве подписи к значку на домашнем экране мобильного устройства. Если оставить пустым, будут использованы первые 12 символов значения настройки, заданной в поле Название конференции.', - 'PWA_SHORT_NAME_INVALID' => 'Заданное значение в поле «Краткое имя сайта» превышает 12 символов.', - 'PWA_ICON_SMALL' => 'Маленький значок для мобильного устройства', - 'PWA_ICON_SMALL_EXPLAIN' => 'Имя файла изображения формата PNG размером 192 x 192 пикселя. Файл изображения должен быть загружен на сервер в папку ' . \phpbb\webpushnotifications\ext::PWA_ICON_DIR . '.', - 'PWA_ICON_LARGE' => 'Большой значок для мобильного устройства', - 'PWA_ICON_LARGE_EXPLAIN' => 'Имя файла изображения формата PNG размером 512 x 512 пикселей. Файл изображения должен быть загружен на сервер в папку ' . \phpbb\webpushnotifications\ext::PWA_ICON_DIR . '.', - 'PWA_ICON_SIZE_INVALID' => 'Изображение «%s» имеет некорректные размеры.', - 'PWA_ICON_MIME_INVALID' => 'Файл изображения «%s» должен иметь формат PNG.', - 'PWA_ICON_INVALID' => 'Файл «%s» не является файлом изображения или отсутствует по указанному пути. Проверьте правильность имени файла и его расположения.', - 'PWA_ICON_NOT_PROVIDED' => 'Настройка «%s» не может быть пустой. Необходимо задать все пути к файлам значков для мобильных устройств.', -]); diff --git a/language/ru/webpushnotifications_module_acp.php b/language/ru/webpushnotifications_module_acp.php index 5232e58..3c5fece 100644 --- a/language/ru/webpushnotifications_module_acp.php +++ b/language/ru/webpushnotifications_module_acp.php @@ -38,7 +38,8 @@ // $lang = array_merge($lang, [ - 'ACP_WEBPUSH_SETTINGS_EXPLAIN' => 'Здесь вы можете включить браузерные push—уведомления. Браузерные push—уведомления — это протокол мгновенной доставки пользователю сообщений о различных событиях. Они совместимы с большинством современных браузеров как на настольных, так и на мобильных устройствах. Пользователи могут включить их в своих браузерах и выбрать предпочтительные виды браузерных уведомлений в личном разделе.
Для работы браузерных push—уведомлений необходимо ввести или сгенерировать ключи идентификации ниже.

Для того, чтобы браузерные push—уведомления стали доступны на мобильных устройствах Apple, конференция должна функционировать как прогрессивное веб—приложение (PWA). Параметры прогрессивного веб—приложения (краткое имя сайта и значки для его отображения на домашнем экране) можно задать в разделе Настройки конференции. После этого пользователи смогут добавлять сайт конференции на домашний экран своего мобильного устройства Apple и получать браузерные push—уведомления.', + // Web push settings + 'ACP_WEBPUSH_SETTINGS_EXPLAIN' => 'Здесь вы можете включить браузерные push—уведомления. Браузерные push—уведомления — это протокол мгновенной доставки пользователю сообщений о различных событиях. Они совместимы с большинством современных браузеров как на настольных, так и на мобильных устройствах. Пользователи могут включить их в своих браузерах и выбрать предпочтительные виды браузерных уведомлений в личном разделе.
Для работы браузерных push—уведомлений необходимо ввести или сгенерировать ключи идентификации ниже.

Для того, чтобы браузерные push—уведомления стали доступны на мобильных устройствах Apple, конференция должна функционировать как прогрессивное веб—приложение (PWA). Параметры прогрессивного веб—приложения (краткое имя сайта и значки для его отображения на домашнем экране) можно задать в разделе PWA settings. После этого пользователи смогут добавлять сайт конференции на домашний экран своего мобильного устройства Apple и получать браузерные push—уведомления.', 'WEBPUSH_ENABLE' => 'Включить браузерные push—уведомления', 'WEBPUSH_ENABLE_EXPLAIN' => 'Включение возможности получения браузерных push—уведомлений для всех пользователей. Для использования браузерных push—уведомлений необходимо задать или сгенерировать корректные ключи идентификации VAPID.', 'WEBPUSH_GENERATE_VAPID_KEYS' => 'Сгенерировать ключи идентификации', @@ -53,4 +54,26 @@ 'WEBPUSH_POPUP_PROMPT' => 'Показывать всплывающее приглашение', 'WEBPUSH_POPUP_PROMPT_EXPLAIN' => 'Показывать всплывающее сообщение зарегистрированным пользователям с приглашением подписаться на браузерные уведомления данной конференции. Сообщение будет показано только тем зарегистрированным пользователям, которые не подписаны на браузерные уведомления и ранее не отклоняли такое приглашение.', 'WEBPUSH_INSECURE_SERVER_ERROR' => 'На данной конференции не применяется защищённый протокол SSL/HTTPS, без которого использование браузерных push—уведомлений невозможно, либо соответствующие переменные серверного окружения неверно сконфигурированы. Убедитесь, что значения переменных серверного окружения HTTPS и/или HEADER_CLIENT_PROTO заданы верно.', + + // PWA Settings + 'ACP_PWA_SETTINGS_EXPLAIN' => 'Here you can configure Progressive Web App behaviour, including app manifest metadata and install banner display.', + 'PWA_SHORT_NAME' => 'Short site name', + 'PWA_SHORT_NAME_EXPLAIN' => 'Your site name in 12 characters or fewer, which may be used as a label for an icon on a mobile device’s home screen. (If this field is left empty, the first 12 characters of the Site name will be used.)', + 'PWA_SHORT_NAME_INVALID' => '“Short site name” exceeds the 12 character limit.', + 'PWA_ICON_SMALL' => 'Small mobile device icon', + 'PWA_ICON_SMALL_EXPLAIN' => 'File name of a 192px x 192px PNG image. This file must be uploaded to your board’s ' . \phpbb\webpushnotifications\ext::PWA_ICON_DIR . ' directory.', + 'PWA_ICON_LARGE' => 'Large mobile device icon', + 'PWA_ICON_LARGE_EXPLAIN' => 'File name of a 512px x 512px PNG image. This file must be uploaded to your board’s ' . \phpbb\webpushnotifications\ext::PWA_ICON_DIR . ' directory.', + 'PWA_ICON_SIZE_INVALID' => '“%s” does not have the correct image dimensions.', + 'PWA_ICON_MIME_INVALID' => '“%s” must be a PNG image file.', + 'PWA_ICON_INVALID' => '“%s” is not a valid image file or is missing from the expected location. Verify the file name and location are correct.', + 'PWA_ICON_NOT_PROVIDED' => '%s field must not be empty. All icon fields must contain an image.', + 'PWA_THEME_COLOURS' => 'Theme & background colours (optional)', + 'PWA_THEME_COLOURS_EXPLAIN' => 'Used to specify the default colour for your web application’s user interface. Theme colours may be applied to various browser UI elements, such as the toolbar, address bar, and status bar. Background colours appears in the application window before your application’s stylesheets have loaded.', + 'PWA_COLOURS' => 'Colours', + 'PWA_THEME_COLOUR' => 'Theme colour', + 'PWA_BACKGROUND_COLOUR' => 'Background colour', + 'PWA_INVALID_COLOUR' => 'The colour code “%s” is not a valid hex code.', + 'PWA_SHOW_INSTALL_BANNER' => 'Show install banner', + 'PWA_SHOW_INSTALL_BANNER_EXPLAIN' => 'Display a mobile install prompt when the browser reports that your board can be installed as an app.', ]); diff --git a/migrations/add_pwa_enhancer_configs.php b/migrations/add_pwa_enhancer_configs.php new file mode 100644 index 0000000..8e7d3c7 --- /dev/null +++ b/migrations/add_pwa_enhancer_configs.php @@ -0,0 +1,52 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + */ + +namespace phpbb\webpushnotifications\migrations; + +use phpbb\db\migration\migration; + +class add_pwa_enhancer_configs extends migration +{ + public function effectively_installed(): bool + { + return $this->config->offsetExists('pwa_show_install_banner'); + } + + public static function depends_on() + { + return ['\phpbb\webpushnotifications\migrations\add_acp_pwa_configs']; + } + + public function update_data(): array + { + return [ + ['config.add', ['pwa_show_install_banner', false]], + ['module.add', ['acp', 'ACP_CLIENT_COMMUNICATION', [ + 'module_basename' => '\phpbb\webpushnotifications\acp\wpn_acp_module', + 'module_langname' => 'ACP_WEBPUSH_PWA_SETTINGS', + 'module_mode' => 'pwa', + 'module_auth' => 'ext_phpbb/webpushnotifications && acl_a_board', + 'after' => 'ACP_WEBPUSH_EXT_SETTINGS', + ]]], + ]; + } + + public function revert_data(): array + { + return [ + ['config.remove', ['pwa_show_install_banner']], + ['module.remove', ['acp', 'ACP_CLIENT_COMMUNICATION', [ + 'module_basename' => '\phpbb\webpushnotifications\acp\wpn_acp_module', + 'module_langname' => 'ACP_WEBPUSH_PWA_SETTINGS', + 'module_mode' => 'pwa', + ]]], + ]; + } +} diff --git a/migrations/add_pwa_enhancer_schema.php b/migrations/add_pwa_enhancer_schema.php new file mode 100644 index 0000000..dde8565 --- /dev/null +++ b/migrations/add_pwa_enhancer_schema.php @@ -0,0 +1,50 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + */ + +namespace phpbb\webpushnotifications\migrations; + +use phpbb\db\migration\migration; + +class add_pwa_enhancer_schema extends migration +{ + public static function depends_on() + { + return ['\phpbb\webpushnotifications\migrations\add_pwa_enhancer_configs']; + } + + public function effectively_installed(): bool + { + return $this->db_tools->sql_column_exists($this->table_prefix . 'styles', 'pwa_bg_color'); + } + + public function update_schema(): array + { + return [ + 'add_columns' => [ + $this->table_prefix . 'styles' => [ + 'pwa_bg_color' => ['VCHAR:8', ''], + 'pwa_theme_color' => ['VCHAR:8', ''], + ], + ], + ]; + } + + public function revert_schema(): array + { + return [ + 'drop_columns' => [ + $this->table_prefix . 'styles' => [ + 'pwa_bg_color', + 'pwa_theme_color', + ], + ], + ]; + } +} diff --git a/styles/all/template/event/overall_footer_body_after.html b/styles/all/template/event/overall_footer_body_after.html index 36ac651..2c4e6c6 100644 --- a/styles/all/template/event/overall_footer_body_after.html +++ b/styles/all/template/event/overall_footer_body_after.html @@ -16,3 +16,24 @@ } {% endif %} + +{% if S_PWA_SHOW_BANNER %} + {% INCLUDEJS '@phpbb_webpushnotifications/pwa_install_banner.js' %} + {% INCLUDECSS '@phpbb_webpushnotifications/phpbb_pwa.css' %} + +{% endif %} diff --git a/styles/all/template/event/overall_header_head_append.html b/styles/all/template/event/overall_header_head_append.html index d92a189..e29b1bb 100644 --- a/styles/all/template/event/overall_header_head_append.html +++ b/styles/all/template/event/overall_header_head_append.html @@ -1,5 +1,8 @@ + + + diff --git a/styles/all/template/pwa_install_banner.js b/styles/all/template/pwa_install_banner.js new file mode 100644 index 0000000..92233d8 --- /dev/null +++ b/styles/all/template/pwa_install_banner.js @@ -0,0 +1,131 @@ +(function() { + 'use strict'; + + const dismissedKey = 'phpbb_wpn_pwa_banner_dismissed'; + let deferredPrompt = null; + + function isStandalone() { + return ((window.matchMedia && window.matchMedia('(display-mode: standalone)').matches) || window.navigator.standalone === true); + } + + function isIOS() { + return /iphone|ipad|ipod/i.test(window.navigator.userAgent); + } + + function isSafari() { + const userAgent = window.navigator.userAgent; + + return (/safari/i.test(userAgent) && !/crios|fxios|edgios|opr\//i.test(userAgent)); + } + + function localStorageGet(key) { + try { + return window.localStorage.getItem(key); + } catch (e) { + return null; + } + } + + function localStorageSet(key, value) { + try { + window.localStorage.setItem(key, value); + } catch (e) { + // Private browsing may block localStorage. + } + } + + function hideInstallBanner(banner) { + if (banner) { + banner.hidden = true; + } + } + + function showIOSInstallInstructions(banner, installButton) { + const defaultSubtitle = document.getElementById('pwa-install-subtitle-default'); + const iosSubtitle = document.getElementById('pwa-install-subtitle-ios'); + + banner.hidden = false; + + if (defaultSubtitle) { + defaultSubtitle.hidden = true; + } + + if (iosSubtitle) { + iosSubtitle.hidden = false; + } + + if (installButton) { + installButton.hidden = true; + } + } + + function setupInstallBanner() { + const banner = document.getElementById('pwa-install-banner'); + const installButton = document.getElementById('pwa-btn-install'); + const dismissButton = document.getElementById('pwa-btn-dismiss'); + + if (!banner || !dismissButton || isStandalone()) { + hideInstallBanner(banner); + return; + } + + if (localStorageGet(dismissedKey) === '1') { + hideInstallBanner(banner); + return; + } + + // iOS Safari does not support beforeinstallprompt. + if (isIOS() && isSafari()) { + showIOSInstallInstructions(banner, installButton); + + dismissButton.addEventListener('click', () => { + hideInstallBanner(banner); + localStorageSet(dismissedKey, '1'); + }); + + return; + } + + // Chromium install flow. + window.addEventListener('beforeinstallprompt', event => { + event.preventDefault(); + deferredPrompt = event; + banner.hidden = false; + }); + + if (installButton) { + installButton.addEventListener('click', () => { + if (!deferredPrompt) { + hideInstallBanner(banner); + return; + } + + deferredPrompt.prompt(); + deferredPrompt.userChoice.then(choice => { + if (choice.outcome === 'accepted') { + hideInstallBanner(banner); + } + + deferredPrompt = null; + }); + }); + } + + dismissButton.addEventListener('click', () => { + hideInstallBanner(banner); + localStorageSet(dismissedKey, '1'); + }); + + window.addEventListener('appinstalled', () => { + hideInstallBanner(banner); + localStorageSet(dismissedKey, '1'); + deferredPrompt = null; + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', setupInstallBanner); + } else { + setupInstallBanner(); + } +})(); diff --git a/styles/all/theme/phpbb_pwa.css b/styles/all/theme/phpbb_pwa.css new file mode 100644 index 0000000..b453fb1 --- /dev/null +++ b/styles/all/theme/phpbb_pwa.css @@ -0,0 +1,90 @@ + +.pwa-install-banner { + font-family: sans-serif; + background: var(--pwa-theme-colour, #000000); + box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.25); + color: #ffffff; + position: fixed; + z-index: 9999; + right: 0; + bottom: 0; + left: 0; + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 16px; + gap: 12px; +} + +.pwa-install-banner[hidden] { + display: none; +} + +.pwa-install-banner__content { + display: flex; + flex: 1; + align-items: center; + min-width: 0; + gap: 10px; +} + +.pwa-install-banner__icon { + border-radius: 10px; + flex-shrink: 0; + width: 44px; + height: 44px; +} + +.pwa-install-banner__text { + font-size: 14px; + font-weight: 500; + line-height: 1.4; + overflow: hidden; + min-width: 0; +} + +.pwa-install-banner__title { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.pwa-install-banner__subtitle { + font-size: 12px; + opacity: 0.85; +} + +.pwa-install-banner__actions { + display: flex; + flex-shrink: 0; + gap: 8px; +} + +.pwa-install-banner__dismiss, +.pwa-install-banner__install { + font-size: 13px; + white-space: nowrap; + border-radius: 4px; + padding: 7px 10px; + cursor: pointer; +} + +.pwa-install-banner__dismiss { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.5); + color: #ffffff; +} + +.pwa-install-banner__install { + font-weight: bold; + background: #ffffff; + border: 0; + color: var(--pwa-theme-colour, #000000); + padding-inline: 12px; +} + +@media all and (display-mode: standalone) { + .pwa-install-banner { + display: none !important; + } +} diff --git a/tests/acp/acp_module_test.php b/tests/acp/acp_module_test.php new file mode 100644 index 0000000..9771866 --- /dev/null +++ b/tests/acp/acp_module_test.php @@ -0,0 +1,828 @@ +extension_manager = new \phpbb_mock_extension_manager( + $phpbb_root_path, + [ + 'phpbb/webpushnotifications' => [ + 'ext_name' => 'phpbb/webpushnotifications', + 'ext_active' => '1', + 'ext_path' => 'ext/phpbb/webpushnotifications/', + ], + ] + ); + $phpbb_extension_manager = $this->extension_manager; + + $this->module_manager = new \phpbb\module\module_manager( + new \phpbb\cache\driver\dummy(), + $this->createMock('\phpbb\db\driver\driver_interface'), + $this->extension_manager, + MODULES_TABLE, + $phpbb_root_path, + $phpEx + ); + + $phpbb_dispatcher = new \phpbb_mock_event_dispatcher(); + + if (!defined('IN_ADMIN')) + { + define('IN_ADMIN', true); + } + + $this->cache = new \phpbb_mock_cache(); + $this->config = new \phpbb\config\config($this->default_config()); + $this->db = $this->createMock('\phpbb\db\driver\driver_interface'); + $this->imagesize = $this->createMock('\FastImageSize\FastImageSize'); + $this->lang = $this->createMock('\phpbb\language\language'); + $this->log = $this->createMock('\phpbb\log\log_interface'); + $this->request = $this->createMock('\phpbb\request\request'); + $this->symfony_request = $this->createMock('\phpbb\symfony_request'); + $this->template = $this->createMock('\phpbb\template\template'); + $this->user = $this->createMock('\phpbb\user'); + $this->root_path = $phpbb_root_path; + $this->style_updates = []; + + $this->user->data = ['user_id' => 42]; + $this->user->ip = '127.0.0.1'; + $this->user->lang = new \phpbb_mock_lang(); + + $this->lang->method('lang') + ->willReturnCallback(function() { + return implode(':', func_get_args()); + }); + + $this->db->method('sql_query') + ->willReturn('result'); + $this->db->method('sql_fetchrowset') + ->with('result') + ->willReturn($this->style_rows()); + $this->db->method('sql_build_array') + ->willReturnCallback(function($query, $row) { + $this->style_updates[] = $row; + return implode(', ', array_map(static function($name, $value) { + return $name . " = '" . $value . "'"; + }, array_keys($row), $row)); + }); + + $this->container = $this->createMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $this->container->method('get') + ->willReturnCallback(function($service) { + return [ + 'cache' => $this->cache, + 'config' => $this->config, + 'dbal.conn' => $this->db, + 'upload_imagesize' => $this->imagesize, + 'language' => $this->lang, + 'log' => $this->log, + 'request' => $this->request, + 'symfony_request' => $this->symfony_request, + 'template' => $this->template, + 'user' => $this->user, + ][$service]; + }); + $this->container->method('getParameter') + ->with('core.root_path') + ->willReturn($phpbb_root_path); + + $phpbb_container = $this->container; + $template = $this->template; + $user = $this->user; + $language = $this->lang; + } + + public function test_module_info(): void + { + self::assertEquals([ + '\\phpbb\\webpushnotifications\\acp\\wpn_acp_module' => [ + 'filename' => '\\phpbb\\webpushnotifications\\acp\\wpn_acp_module', + 'title' => 'ACP_WEBPUSH_EXT_SETTINGS', + 'modes' => [ + 'webpush' => [ + 'title' => 'ACP_WEBPUSH_EXT_SETTINGS', + 'auth' => 'ext_phpbb/webpushnotifications && acl_a_server', + 'cat' => ['ACP_CLIENT_COMMUNICATION'] + ], + 'pwa' => [ + 'title' => 'ACP_WEBPUSH_PWA_SETTINGS', + 'auth' => 'ext_phpbb/webpushnotifications && acl_a_board', + 'cat' => ['ACP_CLIENT_COMMUNICATION'] + ], + ], + ], + ], $this->module_manager->get_module_infos('acp', 'wpn_acp_module')); + } + + public function module_auth_test_data(): array + { + return [ + 'missing extension' => ['ext_foo/bar', false], + 'active extension' => ['ext_phpbb/webpushnotifications', true], + ]; + } + + /** + * @dataProvider module_auth_test_data + */ + public function test_module_auth($module_auth, $expected): void + { + self::assertEquals($expected, \p_master::module_auth($module_auth, 0)); + } + + public function mode_display_data(): array + { + return [ + 'webpush secure server' => [ + 'webpush', + 'adm.php?i=test&mode=webpush', + 'wpn_acp_settings', + 'ACP_WEBPUSH_EXT_SETTINGS', + true, + 'example.com', + $this->webpush_template_vars('adm.php?i=test&mode=webpush'), + false, + '', + ], + 'webpush localhost without https' => [ + 'webpush', + 'adm.php?i=test&mode=webpush', + 'wpn_acp_settings', + 'ACP_WEBPUSH_EXT_SETTINGS', + false, + 'localhost', + $this->webpush_template_vars('adm.php?i=test&mode=webpush'), + false, + '', + ], + 'webpush insecure remote server' => [ + 'webpush', + 'adm.php?i=test&mode=webpush', + 'wpn_acp_settings', + 'ACP_WEBPUSH_EXT_SETTINGS', + false, + 'example.com', + $this->webpush_template_vars('adm.php?i=test&mode=webpush'), + true, + 'WEBPUSH_INSECURE_SERVER_ERROR', + ], + 'pwa settings' => [ + 'pwa', + 'adm.php?i=test&mode=pwa', + 'wpn_acp_pwa', + 'ACP_WEBPUSH_PWA_SETTINGS', + true, + 'example.com', + $this->pwa_template_vars('adm.php?i=test&mode=pwa'), + false, + '', + ], + ]; + } + + /** + * @dataProvider mode_display_data + */ + public function test_main_displays_mode_settings($mode, $u_action, $tpl_name, $page_title, $is_secure, $server_name, array $settings_vars, $has_error, $error_msg): void + { + $this->request->expects($this->once()) + ->method('is_set_post') + ->with('submit') + ->willReturn(false); + + $this->symfony_request->method('isSecure') + ->willReturn($is_secure); + + $this->request->method('server') + ->with('SERVER_NAME') + ->willReturn($server_name); + + $this->template->expects(self::exactly(2)) + ->method('assign_vars') + ->withConsecutive( + [$settings_vars], + [[ + 'S_ERROR' => $has_error, + 'ERROR_MSG' => $error_msg, + ]] + ); + + $module = $this->create_module($u_action); + $module->main('', $mode); + + self::assertSame($tpl_name, $module->tpl_name); + self::assertSame($page_title, $module->page_title); + } + + public function invalid_form_key_data(): array + { + return [ + 'webpush' => ['webpush'], + 'pwa' => ['pwa'], + ]; + } + + /** + * @dataProvider invalid_form_key_data + */ + public function test_main_rejects_invalid_form_key($mode): void + { + self::$valid_form = false; + + $this->request->method('is_set_post') + ->with('submit') + ->willReturn(true); + + $this->setExpectedTriggerError(E_USER_WARNING, 'FORM_INVALID'); + + $this->create_module('adm.php?i=test&mode=' . $mode)->main('', $mode); + } + + public function webpush_save_data(): array + { + $valid = [ + 'wpn_webpush_enable' => 1, + 'wpn_webpush_vapid_public' => str_repeat('a', 25), + 'wpn_webpush_vapid_private' => str_repeat('b', 25), + 'wpn_webpush_dropdown_subscribe' => 1, + 'wpn_webpush_method_enabled' => 0, + 'wpn_webpush_popup_prompt' => 1, + ]; + + return [ + 'valid enabled settings' => [ + $valid, + [ + 'S_ERROR' => false, + 'ERROR_MSG' => '', + ], + [ + 'wpn_webpush_vapid_public' => str_repeat('a', 25), + 'wpn_webpush_vapid_private' => str_repeat('b', 25), + 'wpn_webpush_method_enabled' => 0, + ], + true, + ], + 'masked existing private key is preserved' => [ + array_merge($valid, [ + 'wpn_webpush_vapid_private' => wpn_acp_module::MASKED_PRIVATE_KEY, + ]), + [ + 'S_ERROR' => false, + 'ERROR_MSG' => '', + ], + [ + 'wpn_webpush_vapid_public' => str_repeat('a', 25), + 'wpn_webpush_vapid_private' => 'existing-private-key', + ], + true, + ], + 'invalid enabled VAPID keys are rejected' => [ + array_merge($valid, [ + 'wpn_webpush_vapid_public' => 'short', + 'wpn_webpush_vapid_private' => 'short', + ]), + [ + 'S_ERROR' => true, + 'ERROR_MSG' => 'SETTING_TOO_SHORT
SETTING_TOO_SHORT', + ], + [ + 'wpn_webpush_vapid_public' => 'existing-public-key', + 'wpn_webpush_vapid_private' => 'existing-private-key', + ], + false, + ], + 'disabled webpush skips VAPID validation' => [ + array_merge($valid, [ + 'wpn_webpush_enable' => 0, + 'wpn_webpush_vapid_public' => '', + 'wpn_webpush_vapid_private' => '', + ]), + [ + 'S_ERROR' => false, + 'ERROR_MSG' => '', + ], + [ + 'wpn_webpush_enable' => 0, + 'wpn_webpush_vapid_public' => '', + 'wpn_webpush_vapid_private' => '', + ], + true, + ], + ]; + } + + /** + * @dataProvider webpush_save_data + */ + public function test_save_settings_validates_and_persists_webpush_config(array $input, array $error_vars, array $expected_config, $expect_saved): void + { + $this->config['wpn_webpush_vapid_public'] = 'existing-public-key'; + $this->config['wpn_webpush_vapid_private'] = 'existing-private-key'; + + $this->request->method('variable') + ->with('config', ['' => ''], true) + ->willReturn($input); + + $this->template->expects($this->once()) + ->method('assign_vars') + ->with($error_vars); + + $this->log->expects($expect_saved ? $this->once() : $this->never()) + ->method('add') + ->with('admin', 42, '127.0.0.1', 'LOG_CONFIG_WEBPUSH'); + + if ($expect_saved) + { + $this->setExpectedTriggerError(E_USER_NOTICE, 'CONFIG_UPDATED'); + } + + $this->create_module()->save_settings(); + + foreach ($expected_config as $name => $value) + { + self::assertSame($value, $this->config[$name]); + } + } + + public function pwa_save_data(): array + { + $valid = [ + 'pwa_short_name' => 'Forum', + 'pwa_icon_small' => 'icon192.png', + 'pwa_icon_large' => 'icon512.png', + 'pwa_show_install_banner' => 1, + ]; + + $valid_style_input = [ + 'pwa_bg_color_1' => '#abcdef', + 'pwa_theme_color_1' => '#ABCDEF', + 'pwa_bg_color_2' => '', + 'pwa_theme_color_2' => '#123', + ]; + + $expected_style_updates = [ + [ + 'pwa_bg_color' => '#abcdef', + 'pwa_theme_color' => '#ABCDEF', + ], + [ + 'pwa_bg_color' => '', + 'pwa_theme_color' => '#123', + ], + ]; + + return [ + 'valid icons and style colours' => [ + $valid, + $valid_style_input, + [ + 'icon192.png' => ['width' => 192, 'height' => 192, 'type' => IMAGETYPE_PNG], + 'icon512.png' => ['width' => 512, 'height' => 512, 'type' => IMAGETYPE_PNG], + ], + [ + 'S_ERROR' => false, + 'ERROR_MSG' => '', + ], + [ + 'pwa_short_name' => 'Forum', + 'pwa_icon_small' => 'icon192.png', + 'pwa_icon_large' => 'icon512.png', + 'pwa_show_install_banner' => 1, + ], + $expected_style_updates, + true, + ], + 'valid empty name and icons' => [ + [], + [], + [], + [ + 'S_ERROR' => false, + 'ERROR_MSG' => '', + ], + [ + 'pwa_short_name' => '', + 'pwa_icon_small' => '', + 'pwa_icon_large' => '', + 'pwa_show_install_banner' => 0, + ], + [ + [ + 'pwa_bg_color' => '#fff000', + 'pwa_theme_color' => '#000fff', + ], + [ + 'pwa_bg_color' => '', + 'pwa_theme_color' => '', + ], + ], + true, + ], + 'too long short name rejected after entity decoding' => [ + array_merge($valid, [ + 'pwa_short_name' => '123456789012😀', + ]), + $valid_style_input, + [ + 'icon192.png' => ['width' => 192, 'height' => 192, 'type' => IMAGETYPE_PNG], + 'icon512.png' => ['width' => 512, 'height' => 512, 'type' => IMAGETYPE_PNG], + ], + [ + 'S_ERROR' => true, + 'ERROR_MSG' => 'PWA_SHORT_NAME_INVALID', + ], + [ + 'pwa_short_name' => 'Old', + ], + [], + false, + ], + 'missing small icon rejected' => [ + array_merge($valid, [ + 'pwa_icon_small' => '', + ]), + $valid_style_input, + [], + [ + 'S_ERROR' => true, + 'ERROR_MSG' => 'PWA_ICON_NOT_PROVIDED:PWA_ICON_SMALL', + ], + [ + 'pwa_icon_small' => 'old-small.png', + ], + [], + false, + ], + 'missing large icon rejected' => [ + array_merge($valid, [ + 'pwa_icon_large' => '', + ]), + $valid_style_input, + [], + [ + 'S_ERROR' => true, + 'ERROR_MSG' => 'PWA_ICON_NOT_PROVIDED:PWA_ICON_LARGE', + ], + [ + 'pwa_icon_large' => 'old-large.png', + ], + [], + false, + ], + 'path traversal icon rejected' => [ + array_merge($valid, [ + 'pwa_icon_small' => '../icon192.png', + ]), + $valid_style_input, + [ + 'icon512.png' => ['width' => 512, 'height' => 512, 'type' => IMAGETYPE_PNG], + ], + [ + 'S_ERROR' => true, + 'ERROR_MSG' => 'PWA_ICON_INVALID:../icon192.png', + ], + [ + 'pwa_icon_small' => 'old-small.png', + ], + [], + false, + ], + 'unreadable icon rejected' => [ + $valid, + $valid_style_input, + [ + 'icon192.png' => false, + 'icon512.png' => ['width' => 512, 'height' => 512, 'type' => IMAGETYPE_PNG], + ], + [ + 'S_ERROR' => true, + 'ERROR_MSG' => 'PWA_ICON_INVALID:icon192.png', + ], + [ + 'pwa_icon_small' => 'old-small.png', + ], + [], + false, + ], + 'wrong icon size and MIME rejected' => [ + $valid, + $valid_style_input, + [ + 'icon192.png' => ['width' => 191, 'height' => 192, 'type' => IMAGETYPE_JPEG], + 'icon512.png' => ['width' => 512, 'height' => 511, 'type' => IMAGETYPE_PNG], + ], + [ + 'S_ERROR' => true, + 'ERROR_MSG' => 'PWA_ICON_SIZE_INVALID:icon192.png
PWA_ICON_MIME_INVALID:icon192.png
PWA_ICON_SIZE_INVALID:icon512.png', + ], + [ + 'pwa_icon_small' => 'old-small.png', + 'pwa_icon_large' => 'old-large.png', + ], + [], + false, + ], + 'invalid style colour rejected' => [ + $valid, + array_merge($valid_style_input, [ + 'pwa_theme_color_1' => 'not-hex', + ]), + [ + 'icon192.png' => ['width' => 192, 'height' => 192, 'type' => IMAGETYPE_PNG], + 'icon512.png' => ['width' => 512, 'height' => 512, 'type' => IMAGETYPE_PNG], + ], + [ + 'S_ERROR' => true, + 'ERROR_MSG' => 'PWA_INVALID_COLOUR:not-hex', + ], + [ + 'pwa_short_name' => 'Old', + ], + [], + false, + ], + ]; + } + + /** + * @dataProvider pwa_save_data + */ + public function test_save_pwa_settings_validates_and_persists_pwa_config(array $input, array $style_input, array $image_info, array $error_vars, array $expected_config, array $expected_style_updates, $expect_saved): void + { + $this->request->method('variable') + ->willReturnCallback(function($name, $default = null, $multibyte = false) use ($input, $style_input) { + if ($name === 'config') + { + return $input; + } + + return $style_input[$name] ?? $default; + }); + + $this->imagesize->method('getImageSize') + ->willReturnCallback(function($path) use ($image_info) { + return $image_info[basename($path)]; + }); + + $this->template->expects($this->once()) + ->method('assign_vars') + ->with($error_vars); + + $this->log->expects($expect_saved ? $this->once() : $this->never()) + ->method('add') + ->with('admin', 42, '127.0.0.1', 'LOG_CONFIG_WEBPUSH'); + + if ($expect_saved) + { + $this->setExpectedTriggerError(E_USER_NOTICE, 'CONFIG_UPDATED'); + } + + $this->create_module('adm.php?i=test&mode=pwa')->save_pwa_settings(); + + foreach ($expected_config as $name => $value) + { + self::assertSame($value, $this->config[$name]); + } + + self::assertSame($expected_style_updates, $this->style_updates); + } + + public function hex_colour_data(): array + { + return [ + 'empty colour allowed' => ['', true, []], + 'valid 3 digit colour allowed' => [' #fff ', true, []], + 'valid 6 digit colour allowed' => ['#ABCDEF', true, []], + 'missing hash rejected' => ['123456', false, ['PWA_INVALID_COLOUR:123456']], + 'invalid character rejected' => ['#12345g', false, ['PWA_INVALID_COLOUR:#12345g']], + ]; + } + + /** + * @dataProvider hex_colour_data + */ + public function test_validate_hex_color($colour, $expected, array $expected_errors): void + { + $module = $this->create_module(); + + self::assertSame($expected, $this->invoke_method($module, 'validate_hex_color', [$colour])); + self::assertSame($expected_errors, $this->get_protected_property($module, 'errors')); + } + + public function test_display_errors_returns_false_without_errors(): void + { + $this->template->expects($this->once()) + ->method('assign_vars') + ->with([ + 'S_ERROR' => false, + 'ERROR_MSG' => '', + ]); + + self::assertFalse($this->create_module()->display_errors()); + } + + public function test_display_errors_returns_true_with_joined_errors(): void + { + $module = $this->create_module(); + $this->set_protected_property($module, 'errors', ['first', 'second']); + + $this->template->expects($this->once()) + ->method('assign_vars') + ->with([ + 'S_ERROR' => true, + 'ERROR_MSG' => 'first
second', + ]); + + self::assertTrue($module->display_errors()); + } + + protected function create_module($u_action = 'adm.php?i=test&mode=webpush'): wpn_acp_module + { + $module = new wpn_acp_module(); + $module->u_action = $u_action; + + foreach ([ + 'cache' => $this->cache, + 'config' => $this->config, + 'db' => $this->db, + 'imagesize' => $this->imagesize, + 'lang' => $this->lang, + 'log' => $this->log, + 'request' => $this->request, + 'symfony_request' => $this->symfony_request, + 'template' => $this->template, + 'user' => $this->user, + 'root_path' => $this->root_path, + ] as $property => $value) + { + $this->set_protected_property($module, $property, $value); + } + + return $module; + } + + protected function set_protected_property($object, $property, $value): void + { + $reflection = new \ReflectionClass($object); + $property = $reflection->getProperty($property); + $property->setAccessible(true); + $property->setValue($object, $value); + } + + protected function get_protected_property($object, $property) + { + $reflection = new \ReflectionClass($object); + $property = $reflection->getProperty($property); + $property->setAccessible(true); + + return $property->getValue($object); + } + + protected function invoke_method($object, $method_name, array $parameters = []) + { + $reflection = new \ReflectionClass($object); + $method = $reflection->getMethod($method_name); + $method->setAccessible(true); + + return $method->invokeArgs($object, $parameters); + } + + protected function default_config(): array + { + return [ + 'wpn_webpush_enable' => 1, + 'wpn_webpush_vapid_public' => 'existing-public-key', + 'wpn_webpush_vapid_private' => 'existing-private-key', + 'wpn_webpush_dropdown_subscribe' => 1, + 'wpn_webpush_method_enabled' => 1, + 'wpn_webpush_popup_prompt' => 1, + 'pwa_show_install_banner' => 1, + 'pwa_short_name' => 'Old', + 'pwa_icon_small' => 'old-small.png', + 'pwa_icon_large' => 'old-large.png', + ]; + } + + protected function style_rows(): array + { + return [ + [ + 'style_id' => 1, + 'style_name' => 'prosilver', + 'pwa_bg_color' => '#fff000', + 'pwa_theme_color' => '#000fff', + ], + [ + 'style_id' => 2, + 'style_name' => 'silverfoo', + 'pwa_bg_color' => '', + 'pwa_theme_color' => '', + ], + ]; + } + + protected function webpush_template_vars($u_action): array + { + return [ + 'S_WEBPUSH_ENABLE' => 1, + 'WEBPUSH_VAPID_PUBLIC' => 'existing-public-key', + 'WEBPUSH_VAPID_PRIVATE' => wpn_acp_module::MASKED_PRIVATE_KEY, + 'S_WEBPUSH_DROPDOWN_SUBSCRIBE' => 1, + 'S_WEBPUSH_METHOD_ENABLED' => 1, + 'S_WEBPUSH_POPUP_PROMPT' => 1, + 'U_ACTION' => $u_action, + ]; + } + + protected function pwa_template_vars($u_action): array + { + return [ + 'S_PWA_SHOW_INSTALL_BANNER' => true, + 'PWA_SHORT_NAME' => 'Old', + 'PWA_ICON_SMALL' => 'old-small.png', + 'PWA_ICON_LARGE' => 'old-large.png', + 'STYLES' => $this->style_rows(), + 'U_ACTION' => $u_action, + ]; + } +} + +namespace phpbb\webpushnotifications\acp; + +function add_form_key() +{ +} + +function check_form_key() +{ + return \phpbb\webpushnotifications\tests\acp\acp_module_test::$valid_form; +} diff --git a/tests/controller/controller_manifest_test.php b/tests/controller/controller_manifest_test.php index 8dac316..03258f7 100644 --- a/tests/controller/controller_manifest_test.php +++ b/tests/controller/controller_manifest_test.php @@ -57,6 +57,8 @@ public function manifest_data() 'orientation' => 'portrait', 'start_url' => '/', 'scope' => '/', + 'theme_color' => '#000000', + 'background_color' => '#ffffff', ], ], 'using script path' => [ @@ -73,6 +75,8 @@ public function manifest_data() 'orientation' => 'portrait', 'start_url' => '/foo/', 'scope' => '/foo/', + 'theme_color' => '#000000', + 'background_color' => '#ffffff', ], ], 'with shortname' => [ @@ -87,6 +91,8 @@ public function manifest_data() 'orientation' => 'portrait', 'start_url' => '/', 'scope' => '/', + 'theme_color' => '#000000', + 'background_color' => '#ffffff', ], ], 'without shortname' => [ @@ -101,6 +107,8 @@ public function manifest_data() 'orientation' => 'portrait', 'start_url' => '/', 'scope' => '/', + 'theme_color' => '#000000', + 'background_color' => '#ffffff', ], ], 'with icons' => [ @@ -117,6 +125,8 @@ public function manifest_data() 'orientation' => 'portrait', 'start_url' => '/', 'scope' => '/', + 'theme_color' => '#000000', + 'background_color' => '#ffffff', 'icons' => [ [ 'src' => 'http://images/site_icons/foo_sm.png', diff --git a/tests/controller/controller_webpush_test.php b/tests/controller/controller_webpush_test.php index 2317f42..89cd11f 100644 --- a/tests/controller/controller_webpush_test.php +++ b/tests/controller/controller_webpush_test.php @@ -328,7 +328,11 @@ public function test_worker() $response = $this->controller->worker(); - $this->assertEquals('text/javascript; charset=UTF-8', $response->headers->get('Content-Type')); + $this->assertEquals('application/javascript; charset=UTF-8', $response->headers->get('Content-Type')); + $this->assertStringContainsString('no-store', $response->headers->get('Cache-Control')); + $this->assertStringContainsString('no-cache', $response->headers->get('Cache-Control')); + $this->assertStringContainsString('must-revalidate', $response->headers->get('Cache-Control')); + $this->assertEquals('noindex, nofollow', $response->headers->get('X-Robots-Tag')); $this->assertEquals('rendered_content', $response->getContent()); $this->assertNull($response->headers->get('X-PHPBB-IS-BOT')); } diff --git a/tests/event/listener_test.php b/tests/event/listener_test.php index 94f52b9..1bba37f 100644 --- a/tests/event/listener_test.php +++ b/tests/event/listener_test.php @@ -31,6 +31,9 @@ class listener_test extends \phpbb_database_test_case /** @var \phpbb\language\language */ protected $language; + /** @var \PHPUnit\Framework\MockObject\MockObject|\phpbb\request\request_interface */ + protected $request; + /** @var \PHPUnit\Framework\MockObject\MockObject|\phpbb\template\template */ protected $template; @@ -66,8 +69,12 @@ protected function setUp(): void $this->language = new \phpbb\language\language($lang_loader); $this->template = $this->getMockBuilder('\phpbb\template\template') ->getMock(); - $request = new \phpbb\request\request(); - $request->enable_super_globals(); + $form_request = new \phpbb\request\request(); + $form_request->enable_super_globals(); + $this->request = $this->getMockBuilder('\phpbb\request\request_interface') + ->getMock(); + $this->request->method('server') + ->willReturn(''); $user = new \phpbb\user($this->language, '\phpbb\datetime'); $this->user = $user; $this->user->data['user_form_salt'] = ''; @@ -87,11 +94,19 @@ protected function setUp(): void 'load_notifications' => true, 'allow_board_notifications' => true, 'wpn_webpush_enable' => true, + 'force_server_vars' => false, + 'script_path' => '', + 'sitename' => 'phpBB', + 'cookie_name' => 'phpbb3', + 'pwa_show_install_banner' => true, + 'pwa_icon_small' => '', + 'pwa_icon_large' => '', + 'pwa_short_name' => '', ]); $this->form_helper = new \phpbb\webpushnotifications\form\form_helper( $this->config, - $request, + $form_request, $this->user ); @@ -122,13 +137,12 @@ protected function set_listener() $this->listener = new \phpbb\webpushnotifications\event\listener( $this->config, $this->controller_helper, - $this->imagesize, $this->form_helper, $this->language, + $this->request, $this->template, $this->user, - $this->notifications, - $this->root_path + $this->notifications ); } @@ -145,9 +159,6 @@ public function test_getSubscribedEvents() 'core.page_header_after', 'core.ucp_display_module_before', 'core.acp_main_notice', - 'core.acp_board_config_edit_add', - 'core.acp_board_config_emoji_enabled', - 'core.validate_config_variable', 'core.help_manager_add_block_after', ], array_keys(\phpbb\webpushnotifications\event\listener::getSubscribedEvents())); } @@ -338,6 +349,9 @@ public function test_pwa_manifest() 'U_MANIFEST_URL' => $this->controller_helper->route('phpbb_webpushnotifications_manifest_controller'), 'U_TOUCH_ICON' => ext::PWA_ICON_DIR . '/icon-192x192.png', 'SHORT_SITE_NAME' => 'Test', + 'PWA_THEME_COLOR' => ext::PWA_THEME_COLOR, + 'PWA_BG_COLOR' => ext::PWA_BG_COLOR, + 'S_PWA_SHOW_BANNER' => false, ]); $dispatcher = new \phpbb\event\dispatcher(); @@ -345,181 +359,6 @@ public function test_pwa_manifest() $dispatcher->trigger_event('core.acp_main_notice'); } - public function acp_pwa_options_data() - { - return [ - [ // expected config and mode - 'settings', - ['vars' => ['legend4' => []]], - ['legend_pwa_settings', 'pwa_short_name', 'pwa_icon_small', 'pwa_icon_large', 'legend4'], - ], - [ // unexpected mode - 'foobar', - ['vars' => ['legend4' => []]], - ['legend4'], - ], - [ // unexpected config - 'post', - ['vars' => ['foobar' => []]], - ['foobar'], - ], - [ // unexpected config and mode - 'foobar', - ['vars' => ['foobar' => []]], - ['foobar'], - ], - ]; - } - - /** - * @dataProvider acp_pwa_options_data - */ - public function test_acp_pwa_options($mode, $display_vars, $expected_keys) - { - $this->set_listener(); - - $dispatcher = new \phpbb\event\dispatcher(); - $dispatcher->addListener('core.acp_board_config_edit_add', [$this->listener, 'acp_pwa_options']); - - $event_data = ['display_vars', 'mode']; - $event_data_after = $dispatcher->trigger_event('core.acp_board_config_edit_add', compact($event_data)); - - foreach ($event_data as $expected) - { - self::assertArrayHasKey($expected, $event_data_after); - } - extract($event_data_after, EXTR_OVERWRITE); - - $keys = array_keys($display_vars['vars']); - - self::assertEquals($expected_keys, $keys); - } - - public function validate_pwa_options_data() - { - return [ - [ - 'pwa_options:icons', - ['pwa_icon_small' => '192.png', 'pwa_icon_large' => '512.png'], - [], - ], - [ - 'pwa_options:icons', - ['pwa_icon_small' => '1.png', 'pwa_icon_large' => '512.png'], - ['PWA_ICON_SIZE_INVALID'], - ], - [ - 'pwa_options:icons', - ['pwa_icon_small' => '1.png', 'pwa_icon_large' => '12.png'], - ['PWA_ICON_SIZE_INVALID'], - ], - [ - 'pwa_options:icons', - ['pwa_icon_small' => '192.jpg', 'pwa_icon_large' => '512.gif'], - ['PWA_ICON_MIME_INVALID'], - ], - [ - 'pwa_options:icons', - ['pwa_icon_small' => '', 'pwa_icon_large' => ''], - [], - ], - [ - 'pwa_options:string', - ['pwa_short_name' => 'foo'], - [], - ], - [ - 'pwa_options:string', - ['pwa_short_name' => ''], - [], - ], - [ - 'pwa_options:string', - ['pwa_short_name' => 'foo❤️'], - [], - ], - [ - 'pwa_options:string', - ['pwa_short_name' => 'Фаны phpBB'], - [], - ], - [ - 'pwa_options:string', - ['pwa_short_name' => 'Фаны phpBB Board'], - ['PWA_SHORT_NAME_INVALID'], - ], - [ - 'pwa_options:string', - ['pwa_short_name' => 'foo❤️bar foo bar'], - ['PWA_SHORT_NAME_INVALID'], - ], - [ - 'pwa_options:string', - ['pwa_short_name' => str_repeat('a', 50)], - ['PWA_SHORT_NAME_INVALID'], - ], - ]; - } - - /** - * @dataProvider validate_pwa_options_data - */ - public function test_validate_pwa_options($validate, $cfg_array, $expected_error) - { - $config_name = key($cfg_array); - $config_definition = ['validate' => $validate]; - - $pwa_icon_small = $cfg_array['pwa_icon_small'] ?? ''; - $pwa_icon_large = $cfg_array['pwa_icon_large'] ?? ''; - - [$small_image_name, $small_image_ext] = $pwa_icon_small ? explode('.', $pwa_icon_small, 2) : ['', '']; - [$large_image_name, $large_image_ext] = $pwa_icon_large ? explode('.', $pwa_icon_large, 2) : ['', '']; - - $error = []; - - $this->set_listener(); - - $this->imagesize->expects($pwa_icon_small && $pwa_icon_large ? self::once() : self::never()) - ->method('getImageSize') - ->willReturnMap([ - [$this->root_path . ext::PWA_ICON_DIR . '/', '', false], - [$this->root_path . ext::PWA_ICON_DIR . '/' . $pwa_icon_small, '', ['width' => (int) $small_image_name, 'height' => (int) $small_image_name, 'type' => $small_image_ext === 'png' ? IMAGETYPE_PNG : IMAGETYPE_UNKNOWN]], - [$this->root_path . ext::PWA_ICON_DIR . '/' . $pwa_icon_large, '', ['width' => (int) $large_image_name, 'height' => (int) $large_image_name, 'type' => $large_image_ext === 'png' ? IMAGETYPE_PNG : IMAGETYPE_UNKNOWN]], - ]); - - $dispatcher = new \phpbb\event\dispatcher(); - $dispatcher->addListener('core.validate_config_variable', [$this->listener, 'validate_pwa_options']); - - $event_data = ['cfg_array', 'config_name', 'config_definition', 'error']; - $event_data_after = $dispatcher->trigger_event('core.validate_config_variable', compact($event_data)); - - foreach ($event_data as $expected) - { - self::assertArrayHasKey($expected, $event_data_after); - } - extract($event_data_after, EXTR_OVERWRITE); - - self::assertEquals($expected_error, $error); - } - - public function test_acp_pwa_allow_emoji() - { - $config_name_ary = ['foo']; - $expected = ['foo', 'pwa_short_name']; - - $this->set_listener(); - - $dispatcher = new \phpbb\event\dispatcher(); - $dispatcher->addListener('core.acp_board_config_emoji_enabled', [$this->listener, 'acp_pwa_allow_emoji']); - - $event_data = ['config_name_ary']; - $event_data_after = $dispatcher->trigger_event('core.acp_board_config_emoji_enabled', compact($event_data)); - - extract($event_data_after, EXTR_OVERWRITE); - - self::assertEquals($expected, $config_name_ary); - } - public function test_wpn_faq() { $this->language->add_lang('webpushnotifications_faq', 'phpbb/webpushnotifications'); diff --git a/tests/functional/functional_test.php b/tests/functional/functional_test.php index 5c4d9ec..9ad95c6 100644 --- a/tests/functional/functional_test.php +++ b/tests/functional/functional_test.php @@ -122,12 +122,14 @@ public function test_manifest() 'orientation' => 'portrait', 'start_url' => '/', 'scope' => '/', + 'theme_color' => '#000000', + 'background_color' => '#ffffff', ]; $this->login(); $this->admin_login(); - $crawler = self::request('GET', 'adm/index.php?i=acp_board&mode=settings&sid=' . $this->sid); + $crawler = self::request('GET', 'adm/index.php?i=-phpbb-webpushnotifications-acp-wpn_acp_module&mode=pwa&sid=' . $this->sid); $form_data = [ 'config[pwa_short_name]' => $expected['short_name'], diff --git a/ucp/controller/webpush.php b/ucp/controller/webpush.php index e66a53f..47bf2c5 100644 --- a/ucp/controller/webpush.php +++ b/ucp/controller/webpush.php @@ -283,7 +283,9 @@ public function worker(): Response ]); $response = new Response($content); - $response->headers->set('Content-Type', 'text/javascript; charset=UTF-8'); + $response->headers->set('Content-Type', 'application/javascript; charset=UTF-8'); + $response->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate'); + $response->headers->set('X-Robots-Tag', 'noindex, nofollow'); if (!empty($this->user->data['is_bot'])) {