From 233dc89a8aeea60a917337141db45483f7c28ed4 Mon Sep 17 00:00:00 2001 From: dmzx Date: Tue, 20 Sep 2016 22:38:19 +0200 Subject: [PATCH] Version 2.0.0-RC6 --- README.md | 16 +- adm/style/acp_mchat_globalsettings.html | 38 +- composer.json | 31 +- config/config_2_0_0.yml | 174 ---- config/routing.yml | 45 +- config/services.yml | 25 +- config/tables.yml | 2 +- controller/acp_controller.php | 81 +- controller/main_controller.php | 77 -- core/functions.php | 676 +++++++++---- core/mchat.php | 937 +++++++++++++----- core/settings.php | 100 +- cron/mchat_prune.php | 67 ++ event/acp_listener.php | 22 +- event/main_listener.php | 81 +- ext.php | 71 ++ language/en/common.php | 6 +- language/en/info_acp_mchat.php | 3 +- language/en/mchat.php | 23 +- language/en/mchat_acp.php | 17 +- language/en/mchat_ucp.php | 3 +- language/en/permissions_mchat.php | 8 +- migrations/mchat_2_0_0_rc3.php | 159 +-- migrations/mchat_2_0_0_rc6.php | 77 ++ sounds/README.txt | 6 + sounds/add.mp3 | Bin 13210 -> 6146 bytes sounds/del.mp3 | Bin 7254 -> 8661 bytes sounds/edit.mp3 | Bin 13210 -> 4899 bytes sounds/error.mp3 | Bin 13210 -> 3228 bytes .../dmzx_mchat_messages_define_icons.html | 1 + .../event/overall_header_head_append.html | 2 + styles/Subway/template/mchat_navlink.html | 1 + styles/Subway/theme/mchat_custom.css | 16 + .../javascript/jquery.auto-grow-input.js | 47 - .../javascript/jquery.autogrow-textarea.js | 211 ++++ .../javascript/js.cookie-2.0.4.min.js | 2 - styles/all/template/javascript/mchat.js | 635 +++++++----- .../template/mchat_script_data.html | 52 +- styles/basic/template/mchat_navlink.html | 2 +- styles/basic/theme/mchat_custom.css | 46 +- .../dmzx_mchat_messages_define_icons.html | 1 + styles/black/template/mchat_navlink.html | 2 +- styles/black/theme/mchat_custom.css | 4 - .../template/event/overall_footer_after.html | 3 + .../event/overall_header_head_append.html | 2 + styles/canvas/theme/mchat_custom.css | 28 + styles/elegance/template/mchat_navlink.html | 2 +- styles/latte/template/mchat_navlink.html | 2 +- styles/metro_blue/template/mchat_navlink.html | 2 +- styles/pbtech/template/mchat_navlink.html | 2 +- styles/pbtech/theme/mchat_custom.css | 2 +- .../dmzx_mchat_messages_define_icons.html | 1 + styles/pbwow3/template/mchat_navlink.html | 2 +- styles/pbwow3/theme/images/message_icons.png | Bin 1939 -> 0 bytes styles/pbwow3/theme/mchat_custom.css | 4 - .../event/dmzx_mchat_custom_include.html | 3 + .../dmzx_mchat_messages_define_icons.html | 1 + .../event/index_body_block_online_append.html | 2 +- .../overall_footer_copyright_append.html | 3 +- .../overall_header_navigation_append.html | 2 +- styles/prosilver/template/mchat_body.html | 34 +- styles/prosilver/template/mchat_header.html | 2 +- styles/prosilver/template/mchat_messages.html | 7 +- .../template/mchat_messages_icons.html | 26 +- styles/prosilver/template/mchat_navlink.html | 4 +- styles/prosilver/template/mchat_panel.html | 42 +- styles/prosilver/template/mchat_whois.html | 4 +- ...sage_icons.png => message_icons_black.png} | Bin .../theme/images/message_icons_white.png} | Bin styles/prosilver/theme/mchat.css | 105 +- styles/simplicity/template/mchat_navlink.html | 2 +- ucp/ucp_mchat_info.php | 2 +- ucp/ucp_mchat_module.php | 2 +- 73 files changed, 2722 insertions(+), 1336 deletions(-) delete mode 100644 config/config_2_0_0.yml delete mode 100644 controller/main_controller.php create mode 100644 cron/mchat_prune.php create mode 100644 migrations/mchat_2_0_0_rc6.php create mode 100644 sounds/README.txt create mode 100644 styles/Subway/template/event/dmzx_mchat_messages_define_icons.html create mode 100644 styles/Subway/template/event/overall_header_head_append.html create mode 100644 styles/Subway/template/mchat_navlink.html create mode 100644 styles/Subway/theme/mchat_custom.css delete mode 100644 styles/all/template/javascript/jquery.auto-grow-input.js create mode 100644 styles/all/template/javascript/jquery.autogrow-textarea.js delete mode 100644 styles/all/template/javascript/js.cookie-2.0.4.min.js rename styles/{prosilver => all}/template/mchat_script_data.html (51%) create mode 100644 styles/black/template/event/dmzx_mchat_messages_define_icons.html create mode 100644 styles/canvas/template/event/overall_footer_after.html create mode 100644 styles/canvas/template/event/overall_header_head_append.html create mode 100644 styles/canvas/theme/mchat_custom.css create mode 100644 styles/pbwow3/template/event/dmzx_mchat_messages_define_icons.html delete mode 100644 styles/pbwow3/theme/images/message_icons.png create mode 100644 styles/prosilver/template/event/dmzx_mchat_custom_include.html create mode 100644 styles/prosilver/template/event/dmzx_mchat_messages_define_icons.html rename styles/prosilver/theme/images/{message_icons.png => message_icons_black.png} (100%) rename styles/{black/theme/images/message_icons.png => prosilver/theme/images/message_icons_white.png} (100%) diff --git a/README.md b/README.md index 52d794d..7ca1ea0 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,23 @@ -# mChat Extension +phpBB Extension - mChat +===================== -[![Build Status](https://travis-ci.org/dmzx/mChat-Extension.svg?branch=master)](https://travis-ci.org/dmzx/mChat-Extension) +[![Build Status](https://travis-ci.org/kasimi/mChat.svg?branch=master)](https://travis-ci.org/kasimi/mChat) ## Install -1. Download the latest release. +1. Download the [latest release](https://github.com/kasimi/mChat/releases). 2. Unzip the downloaded release, and change the name of the folder to `mchat`. -3. In the `ext` directory of your phpBB board, create a new directory named `dmzx` (if it does not already exist). -4. Copy the `mchat` folder to `/ext/dmzx/` (if done correctly, you'll have the main extension class at (your forum root)/ext/dmzx/mchat/composer.json). +3. In the `ext` directory of your phpBB board, create a new directory named `dmzx` if it does not already exist. +4. Copy the `mchat` folder to `/ext/dmzx/`. If done correctly, the folder structure should look like this: `your forum root)/ext/dmzx/mchat/composer.json`. 5. Navigate in the ACP to `Customise -> Manage extensions`. -6. Look for `mChat Extension` under the Disabled Extensions list, and click its `Enable` link. +6. Look for `mChat` under the `Disabled Extensions` list, and click its `Enable` link. ## Uninstall 1. Navigate in the ACP to `Customise -> Extension Management -> Extensions`. -2. Look for `mChat Extension` under the Enabled Extensions list, and click its `Disable` link. +2. Look for `mChat` under the `Enabled Extensions` list, and click its `Disable` link. 3. To permanently uninstall, click `Delete Data` and then delete the `/ext/dmzx/mchat` folder. ## License + [GNU General Public License v2](http://opensource.org/licenses/GPL-2.0) diff --git a/adm/style/acp_mchat_globalsettings.html b/adm/style/acp_mchat_globalsettings.html index 0e83c81..ecd9660 100644 --- a/adm/style/acp_mchat_globalsettings.html +++ b/adm/style/acp_mchat_globalsettings.html @@ -49,6 +49,11 @@
+
+
+
+
+
{L_MCHAT_SETTINGS_ARCHIVE} @@ -61,7 +66,7 @@
{L_MCHAT_SETTINGS_MESSAGES}
-

+

{L_MCHAT_TIMEOUT_EXPLAIN}
@@ -115,7 +120,7 @@

- {L_MCHAT_STATIC_MESSAGE_EXPLAIN}
+ {L_MCHAT_STATIC_MESSAGE_EXPLAIN}
@@ -149,15 +154,23 @@ +
+
+
+ + +
+
{L_MCHAT_SETTINGS_STATS}

- {L_MCHAT_WHOIS_REFRESH_EXPLAIN}
+ {L_MCHAT_WHOIS_REFRESH_EXPLAIN}
+
{L_MCHAT_SETTINGS_PRUNE}
@@ -167,10 +180,19 @@
-
+

+ {L_MCHAT_PRUNE_NUM_EXPLAIN}
- +
+
+
+ + + + +
+
@@ -180,12 +202,12 @@
-
+

-   - +   + {S_FORM_TOKEN}

diff --git a/composer.json b/composer.json index 4ab54d4..5bb982a 100644 --- a/composer.json +++ b/composer.json @@ -1,45 +1,42 @@ { "name": "dmzx/mchat", "type": "phpbb-extension", - "description": "mChat Extension", - "homepage": "http://www.dmzx-web.net", - "version": "2.0.0-RC5", - "time": "2016-04-03", + "description": "mChat", + "homepage": "https://github.com/kasimi/mChat", + "version": "2.0.0-RC6", + "time": "2016-09-18", "keywords": ["phpbb", "extension", "mchat"], "license": "GPL-2.0", "authors": [ + { + "name": "kasimi", + "homepage": "https://kasimi.net", + "email": "mail@kasimi.net", + "role": "Extension Developer" + }, { "name": "dmzx", "homepage": "http://www.dmzx-web.net", "email": "info@dmzx-web.net", "role": "Extension Developer" }, - { - "name": "kasimi", - "homepage": "https://www.phpbb.com/community/memberlist.php?mode=viewprofile&u=1330603", - "role": "Extension Co-Developer" - }, { "name": "RMcGirr83", "homepage": "http://rmcgirr83.org", - "role": "Author" + "role": "Original MOD author" } ], "require": { - "php": ">=5.3.3" + "php": ">=5.3.3", + "phpbb/phpbb": ">=3.1.7-PL1,<3.3.0@dev" }, "require-dev": { "phpbb/epv": "dev-master" }, "extra": { - "display-name": "mChat Extension", + "display-name": "mChat", "soft-require": { "phpbb/phpbb": ">=3.1.7-PL1,<3.3.0@dev" - }, - "version-check": { - "host": "www.dmzx-web.net", - "directory": "/versions", - "filename": "mchat_version.json" } } } diff --git a/config/config_2_0_0.yml b/config/config_2_0_0.yml deleted file mode 100644 index 927cc06..0000000 --- a/config/config_2_0_0.yml +++ /dev/null @@ -1,174 +0,0 @@ -parameters: - # Global settings that only the administrator is allowed to modify. - # The values are stored in the phpbb_config table and can be - # accessed using the \phpbb\config\config $config class. - dmzx.mchat.config_global: - mchat_bbcode_disallowed: - default: '' - validation: - - 'string' - - false - - 0 - - 255 - mchat_custom_height: - default: 350 - validation: - - 'num' - - false - - 50 - - 1000 - mchat_custom_page: - default: 1 - mchat_edit_delete_limit: - default: 0 - mchat_flood_time: - default: 0 - validation: - - 'num' - - false - - 0 - - 60 - mchat_index_height: - default: 250 - validation: - - 'num' - - false - - 50 - - 1000 - mchat_live_updates: - default: 1 - mchat_max_message_lngth: - default: 500 - validation: - - 'num' - - false - - 0 - - 1000 - mchat_message_num_archive: - default: 25 - validation: - - 'num' - - false - - 10 - - 100 - mchat_message_num_custom: - default: 10 - validation: - - 'num' - - false - - 5 - - 50 - mchat_message_num_index: - default: 10 - validation: - - 'num' - - false - - 5 - - 50 - mchat_navbar_link: - default: 1 - mchat_override_min_post_chars: - default: 0 - mchat_override_smilie_limit: - default: 0 - mchat_posts_edit: - default: 0 - mchat_posts_quote: - default: 0 - mchat_posts_reply: - default: 0 - mchat_posts_topic: - default: 0 - mchat_prune: - default: 0 - mchat_prune_num: - default: 0 - mchat_refresh: - default: 10 - validation: - - 'num' - - false - - 5 - - 60 - mchat_rules: - default: '' - validation: - - 'string' - - false - - 0 - - 255 - mchat_static_message: - default: '' - validation: - - 'string' - - false - - 0 - - 255 - mchat_timeout: - default: 0 - validation: - - 'num' - - false - - 0 - - -1 # This value is replaced with $config['session_length'] in the \dmzx\mchat\core\settings class - mchat_whois: - default: 1 - mchat_whois_refresh: - default: 60 - validation: - - 'num' - - false - - 10 - - 300 - - # User-specific settings for which the administrator can set default - # values as well as adjust permissions to allow users to customize them. - # For each setting a new column is added to the phpbb_users table. - dmzx.mchat.config_ucp: - mchat_avatars: - default: 1 - type: 'BOOL' - mchat_capital_letter: - default: 1 - type: 'BOOL' - mchat_character_count: - default: 1 - type: 'BOOL' - mchat_date: - default: 'D M d, Y g:i a' - type: 'VCHAR:64' - validation: - - 'string' - - false - - 0 - - 64 - mchat_index: - default: 1 - type: 'BOOL' - mchat_input_area: - default: 1 - type: 'BOOL' - mchat_location: - default: 1 - type: 'BOOL' - mchat_message_top: - default: 1 - type: 'BOOL' - mchat_pause_on_input: - default: 0 - type: 'BOOL' - mchat_posts: - default: 1 - type: 'BOOL' - mchat_relative_time: - default: 1 - type: 'BOOL' - mchat_sound: - default: 1 - type: 'BOOL' - mchat_stats_index: - default: 0 - type: 'BOOL' - mchat_whois_index: - default: 1 - type: 'BOOL' diff --git a/config/routing.yml b/config/routing.yml index 74d1ad9..10e31df 100644 --- a/config/routing.yml +++ b/config/routing.yml @@ -1,16 +1,37 @@ -dmzx_mchat_controller: +dmzx_mchat_page_custom_controller: path: /mchat methods: [GET] - defaults: { _controller: dmzx.mchat.main.controller:page, page: custom } -dmzx_mchat_page_controller: - path: /mchat/{page} + defaults: { _controller: dmzx.mchat.core:page_custom } +dmzx_mchat_page_archive_controller: + path: /mchat/archive methods: [GET] - defaults: { _controller: dmzx.mchat.main.controller:page } - requirements: - page: 'archive|rules|whois' -dmzx_mchat_action_controller: - path: /mchat/action-{action} + defaults: { _controller: dmzx.mchat.core:page_archive } +dmzx_mchat_page_rules_controller: + path: /mchat/rules + methods: [GET] + defaults: { _controller: dmzx.mchat.core:page_rules } +dmzx_mchat_page_whois_controller: + path: /mchat/whois/{ip} + methods: [GET] + defaults: { _controller: dmzx.mchat.core:page_whois } + +dmzx_mchat_action_add_controller: + path: /mchat/action/add methods: [POST] - defaults: { _controller: dmzx.mchat.main.controller:action } - requirements: - action: 'add|edit|del|refresh|whois' + defaults: { _controller: dmzx.mchat.core:action_add } +dmzx_mchat_action_edit_controller: + path: /mchat/action/edit + methods: [POST] + defaults: { _controller: dmzx.mchat.core:action_edit } +dmzx_mchat_action_del_controller: + path: /mchat/action/del + methods: [POST] + defaults: { _controller: dmzx.mchat.core:action_del } +dmzx_mchat_action_refresh_controller: + path: /mchat/action/refresh + methods: [POST] + defaults: { _controller: dmzx.mchat.core:action_refresh } +dmzx_mchat_action_whois_controller: + path: /mchat/action/whois + methods: [POST] + defaults: { _controller: dmzx.mchat.core:action_whois } diff --git a/config/services.yml b/config/services.yml index a9e5731..fdf49be 100644 --- a/config/services.yml +++ b/config/services.yml @@ -1,11 +1,11 @@ imports: - { resource: tables.yml } - - { resource: config_2_0_0.yml } services: dmzx.mchat.acp.controller: class: dmzx\mchat\controller\acp_controller arguments: + - '@dmzx.mchat.functions' - '@template' - '@log' - '@user' @@ -14,7 +14,7 @@ services: - '@request' - '@dmzx.mchat.settings' - '%dmzx.mchat.table.mchat%' - - '%dmzx.mchat.table.mchat_deleted_messages%' + - '%dmzx.mchat.table.mchat_log%' - '%core.root_path%' - '%core.php_ext%' dmzx.mchat.ucp.controller: @@ -28,12 +28,6 @@ services: - '@dmzx.mchat.settings' - '%core.root_path%' - '%core.php_ext%' - dmzx.mchat.main.controller: - class: dmzx\mchat\controller\main_controller - arguments: - - '@user' - - '@dmzx.mchat.core' - - '@request' dmzx.mchat.core: class: dmzx\mchat\core\mchat arguments: @@ -59,10 +53,11 @@ services: - '@log' - '@dbal.conn' - '@cache.driver' + - '@dispatcher' - '%core.root_path%' - '%core.php_ext%' - '%dmzx.mchat.table.mchat%' - - '%dmzx.mchat.table.mchat_deleted_messages%' + - '%dmzx.mchat.table.mchat_log%' - '%dmzx.mchat.table.mchat_sessions%' dmzx.mchat.settings: class: dmzx\mchat\core\settings @@ -70,8 +65,6 @@ services: - '@user' - '@config' - '@auth' - - '%dmzx.mchat.config_global%' - - '%dmzx.mchat.config_ucp%' dmzx.mchat.acp.listener: class: dmzx\mchat\event\acp_listener arguments: @@ -89,6 +82,16 @@ services: - '@dmzx.mchat.core' - '@controller.helper' - '@user' + - '@request' - '%core.php_ext%' tags: - { name: event.listener } + dmzx.mchat.cron.task.mchat_prune: + class: dmzx\mchat\cron\mchat_prune + arguments: + - '@dmzx.mchat.functions' + - '@dmzx.mchat.settings' + calls: + - [set_name, [cron.task.mchat_prune]] + tags: + - { name: cron.task } diff --git a/config/tables.yml b/config/tables.yml index 29d1a82..d88d30a 100644 --- a/config/tables.yml +++ b/config/tables.yml @@ -1,4 +1,4 @@ parameters: dmzx.mchat.table.mchat: %core.table_prefix%mchat - dmzx.mchat.table.mchat_deleted_messages: %core.table_prefix%mchat_deleted_messages + dmzx.mchat.table.mchat_log: %core.table_prefix%mchat_log dmzx.mchat.table.mchat_sessions: %core.table_prefix%mchat_sessions diff --git a/controller/acp_controller.php b/controller/acp_controller.php index 47bad30..40e84bf 100644 --- a/controller/acp_controller.php +++ b/controller/acp_controller.php @@ -13,6 +13,9 @@ namespace dmzx\mchat\controller; class acp_controller { + /** @var \dmzx\mchat\core\functions */ + protected $functions; + /** @var \phpbb\template\template */ protected $template; @@ -38,7 +41,7 @@ class acp_controller protected $mchat_table; /** @var string */ - protected $mchat_deleted_messages_table; + protected $mchat_log_table; /** @var string */ protected $root_path; @@ -49,6 +52,7 @@ class acp_controller /** * Constructor * + * @param \dmzx\mchat\core\functions $functions * @param \phpbb\template\template $template * @param \phpbb\log\log_interface $log * @param \phpbb\user $user @@ -57,23 +61,24 @@ class acp_controller * @param \phpbb\request\request $request * @param \dmzx\mchat\core\settings $settings * @param string $mchat_table - * @param string $mchat_deleted_messages_table + * @param string $mchat_log_table * @param string $root_path * @param string $php_ext */ - public function __construct(\phpbb\template\template $template, \phpbb\log\log_interface $log, \phpbb\user $user, \phpbb\db\driver\driver_interface $db, \phpbb\cache\service $cache, \phpbb\request\request $request, \dmzx\mchat\core\settings $settings, $mchat_table, $mchat_deleted_messages_table, $root_path, $php_ext) + public function __construct(\dmzx\mchat\core\functions $functions, \phpbb\template\template $template, \phpbb\log\log_interface $log, \phpbb\user $user, \phpbb\db\driver\driver_interface $db, \phpbb\cache\service $cache, \phpbb\request\request $request, \dmzx\mchat\core\settings $settings, $mchat_table, $mchat_log_table, $root_path, $php_ext) { - $this->template = $template; - $this->log = $log; - $this->user = $user; - $this->db = $db; - $this->cache = $cache; - $this->request = $request; - $this->settings = $settings; - $this->mchat_table = $mchat_table; - $this->mchat_deleted_messages_table = $mchat_deleted_messages_table; - $this->root_path = $root_path; - $this->php_ext = $php_ext; + $this->functions = $functions; + $this->template = $template; + $this->log = $log; + $this->user = $user; + $this->db = $db; + $this->cache = $cache; + $this->request = $request; + $this->settings = $settings; + $this->mchat_table = $mchat_table; + $this->mchat_log_table = $mchat_log_table; + $this->root_path = $root_path; + $this->php_ext = $php_ext; } /** @@ -87,15 +92,9 @@ class acp_controller $error = array(); - if ($this->request->is_set_post('mchat_purge') && $this->request->variable('mchat_purge_confirm', false) && check_form_key('acp_mchat') && $this->user->data['user_type'] == USER_FOUNDER) - { - $this->db->sql_query('TRUNCATE TABLE ' . $this->mchat_table); - $this->db->sql_query('TRUNCATE TABLE ' . $this->mchat_deleted_messages_table); - $this->cache->destroy('sql', $this->mchat_deleted_messages_table); - $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_MCHAT_TABLE_PURGED', false, array($this->user->data['username'])); - trigger_error($this->user->lang('MCHAT_PURGED') . adm_back_link($u_action)); - } - else if ($this->request->is_set_post('submit')) + $is_founder = $this->user->data['user_type'] == USER_FOUNDER; + + if ($this->request->is_set_post('submit')) { $mchat_new_config = array(); $validation = array(); @@ -110,6 +109,13 @@ class acp_controller } } + // Don't allow changing pruning settings for non founders + if (!$is_founder) + { + unset($mchat_new_config['mchat_prune']); + unset($mchat_new_config['mchat_prune_num']); + } + if (!function_exists('validate_data')) { include($this->root_path . 'includes/functions_user.' . $this->php_ext); @@ -140,17 +146,34 @@ class acp_controller $error = array_map(array($this->user, 'lang'), $error); } + if (!$error) + { + if ($is_founder && $this->request->is_set_post('mchat_purge') && $this->request->variable('mchat_purge_confirm', false) && check_form_key('acp_mchat')) + { + $this->db->sql_query('TRUNCATE TABLE ' . $this->mchat_table); + $this->db->sql_query('TRUNCATE TABLE ' . $this->mchat_log_table); + $this->cache->destroy('sql', $this->mchat_log_table); + $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_MCHAT_TABLE_PURGED', false, array($this->user->data['username'])); + trigger_error($this->user->lang('MCHAT_PURGED') . adm_back_link($u_action)); + } + else if ($is_founder && $this->request->is_set_post('mchat_prune_now') && $this->request->variable('mchat_prune_now_confirm', false) && check_form_key('acp_mchat')) + { + $num_pruned_messages = count($this->functions->mchat_prune()); + trigger_error($this->user->lang('MCHAT_PRUNED', $num_pruned_messages) . adm_back_link($u_action)); + } + } + foreach (array_keys($this->settings->global) as $key) { $this->template->assign_var(strtoupper($key), $this->settings->cfg($key)); } $this->template->assign_vars(array( - 'MCHAT_ERROR' => $error ? implode('
', $error) : '', + 'MCHAT_ERROR' => implode('
', $error), 'MCHAT_VERSION' => $this->settings->cfg('mchat_version'), - 'MCHAT_FOUNDER' => $this->user->data['user_type'] == USER_FOUNDER, + 'MCHAT_FOUNDER' => $is_founder, 'L_MCHAT_BBCODES_DISALLOWED_EXPLAIN' => $this->user->lang('MCHAT_BBCODES_DISALLOWED_EXPLAIN', 'root_path}adm/index.$this->php_ext", 'i=bbcodes', true, $this->user->session_id) . '">', ''), - 'L_MCHAT_TIMEOUT_EXPLAIN' => $this->user->lang('MCHAT_USER_TIMEOUT_EXPLAIN','root_path}adm/index.$this->php_ext", 'i=board&mode=load', true, $this->user->session_id) . '">', '', $this->settings->cfg('session_length')), + 'L_MCHAT_TIMEOUT_EXPLAIN' => $this->user->lang('MCHAT_TIMEOUT_EXPLAIN','root_path}adm/index.$this->php_ext", 'i=board&mode=load', true, $this->user->session_id) . '">', '', $this->settings->cfg('session_length')), 'U_ACTION' => $u_action, )); } @@ -238,9 +261,9 @@ class acp_controller $this->template->assign_var('MCHAT_POSTS_ENABLED_LANG', $notifications_template_data); $this->template->assign_vars(array( - 'MCHAT_ERROR' => $error ? implode('
', $error) : '', - 'MCHAT_VERSION' => $this->settings->cfg('mchat_version'), - 'U_ACTION' => $u_action, + 'MCHAT_ERROR' => implode('
', $error), + 'MCHAT_VERSION' => $this->settings->cfg('mchat_version'), + 'U_ACTION' => $u_action, )); } } diff --git a/controller/main_controller.php b/controller/main_controller.php deleted file mode 100644 index 45644a1..0000000 --- a/controller/main_controller.php +++ /dev/null @@ -1,77 +0,0 @@ -user = $user; - $this->mchat = $mchat; - $this->request = $request; - } - - /** - * Controller for mChat - * - * @param string $page The page to render, one of custom|archive|rules|whois - * @return \Symfony\Component\HttpFoundation\Response A Symfony Response object - */ - public function page($page) - { - $this->user->add_lang_ext('dmzx/mchat', 'mchat'); - return call_user_func(array($this->mchat, 'page_' . $page)); - } - - /** - * Controller for mChat actions called with Ajax requests - * - * @param string $action The action to perform, one of add|edit|del|refresh|whois - * @return A Symfony JsonResponse object. - */ - public function action($action) - { - if (!$this->request->is_ajax()) - { - throw new \phpbb\exception\http_exception(403, 'NO_AUTH_OPERATION'); - } - - // Fix avatars & smilies - if (!defined('PHPBB_USE_BOARD_URL_PATH')) - { - define('PHPBB_USE_BOARD_URL_PATH', true); - } - - $this->user->add_lang_ext('dmzx/mchat', 'mchat'); - $data = call_user_func(array($this->mchat, 'action_' . $action)); - - return new JsonResponse($data); - } -} diff --git a/core/functions.php b/core/functions.php index 46df26e..6421281 100644 --- a/core/functions.php +++ b/core/functions.php @@ -31,6 +31,9 @@ class functions /** @var \phpbb\cache\driver\driver_interface */ protected $cache; + /** @var \phpbb\event\dispatcher_interface */ + protected $dispatcher; + /** @var string */ protected $root_path; @@ -41,13 +44,28 @@ class functions protected $mchat_table; /** @var string */ - protected $mchat_deleted_messages_table; + protected $mchat_log_table; /** @var string */ protected $mchat_sessions_table; /** @var array */ - protected $foes = null; + public $log_types = array( + 1 => 'edit', + 2 => 'del', + ); + + /** + * Value of the phpbb_mchat.post_id field for login notification + * messages if the user session is visible at the time of login + */ + const LOGIN_VISIBLE = 1; + + /** + * Value of the phpbb_mchat.post_id field for login notification + * messages if the user session is hidden at the time of login + */ + const LOGIN_HIDDEN = 2; /** * Constructor @@ -58,25 +76,27 @@ class functions * @param \phpbb\log\log_interface $log * @param \phpbb\db\driver\driver_interface $db * @param \phpbb\cache\driver\driver_interface $cache + * @param \phpbb\event\dispatcher_interface $dispatcher * @param string $root_path * @param string $php_ext * @param string $mchat_table - * @param string $mchat_deleted_messages_table + * @param string $mchat_log_table * @param string $mchat_sessions_table */ - function __construct(\dmzx\mchat\core\settings $settings, \phpbb\user $user, \phpbb\auth\auth $auth, \phpbb\log\log_interface $log, \phpbb\db\driver\driver_interface $db, \phpbb\cache\driver\driver_interface $cache, $root_path, $php_ext, $mchat_table, $mchat_deleted_messages_table, $mchat_sessions_table) + function __construct(\dmzx\mchat\core\settings $settings, \phpbb\user $user, \phpbb\auth\auth $auth, \phpbb\log\log_interface $log, \phpbb\db\driver\driver_interface $db, \phpbb\cache\driver\driver_interface $cache, \phpbb\event\dispatcher_interface $dispatcher, $root_path, $php_ext, $mchat_table, $mchat_log_table, $mchat_sessions_table) { - $this->settings = $settings; - $this->user = $user; - $this->auth = $auth; - $this->log = $log; - $this->db = $db; - $this->cache = $cache; - $this->root_path = $root_path; - $this->php_ext = $php_ext; - $this->mchat_table = $mchat_table; - $this->mchat_deleted_messages_table = $mchat_deleted_messages_table; - $this->mchat_sessions_table = $mchat_sessions_table; + $this->settings = $settings; + $this->user = $user; + $this->auth = $auth; + $this->log = $log; + $this->db = $db; + $this->cache = $cache; + $this->dispatcher = $dispatcher; + $this->root_path = $root_path; + $this->php_ext = $php_ext; + $this->mchat_table = $mchat_table; + $this->mchat_log_table = $mchat_log_table; + $this->mchat_sessions_table = $mchat_sessions_table; } /** @@ -115,7 +135,7 @@ class functions /** * Returns the total session time in seconds * - * @return string + * @return int */ protected function mchat_session_time() { @@ -141,25 +161,52 @@ class functions */ public function mchat_active_users() { - $mchat_users = array(); - $check_time = time() - $this->mchat_session_time(); - $sql = 'SELECT m.user_id, u.username, u.user_type, u.user_allow_viewonline, u.user_colour - FROM ' . $this->mchat_sessions_table . ' m - LEFT JOIN ' . USERS_TABLE . ' u ON m.user_id = u.user_id - WHERE m.user_lastupdate >= ' . (int) $check_time . ' - ORDER BY u.username ASC'; + $sql_array = array( + 'SELECT' => 'u.user_id, u.username, u.user_colour, s.session_viewonline', + 'FROM' => array( + $this->mchat_sessions_table => 'ms' + ), + 'LEFT_JOIN' => array( + array( + 'FROM' => array(SESSIONS_TABLE => 's'), + 'ON' => 'ms.user_id = s.session_user_id', + ), + array( + 'FROM' => array(USERS_TABLE => 'u'), + 'ON' => 'ms.user_id = u.user_id', + ), + ), + 'WHERE' => 'u.user_id <> ' . ANONYMOUS . ' AND s.session_viewonline IS NOT NULL AND ms.user_lastupdate > ' . (int) $check_time, + 'ORDER_BY' => 'u.username ASC', + ); + + /** + * Event to modify the SQL query that fetches active mChat users + * + * @event dmzx.mchat.active_users_sql_before + * @var array sql_array Array with SQL query data to fetch the current active sessions + * @since 2.0.0-RC6 + */ + $vars = array( + 'sql_array', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.active_users_sql_before', compact($vars))); + + $sql = $this->db->sql_build_query('SELECT', $sql_array); $result = $this->db->sql_query($sql); $rows = $this->db->sql_fetchrowset($result); $this->db->sql_freeresult($result); + $mchat_users = array(); $can_view_hidden = $this->auth->acl_get('u_viewonline'); + foreach ($rows as $row) { - if (!$row['user_allow_viewonline']) + if (!$row['session_viewonline']) { - if (!$can_view_hidden) + if (!$can_view_hidden && $row['user_id'] !== $this->user->data['user_id']) { continue; } @@ -167,87 +214,146 @@ class functions $row['username'] = '' . $row['username'] . ''; } - $mchat_users[] = get_username_string('full', $row['user_id'], $row['username'], $row['user_colour'], $this->user->lang('GUEST')); + $mchat_users[$row['user_id']] = get_username_string('full', $row['user_id'], $row['username'], $row['user_colour'], $this->user->lang('GUEST')); } - return array( + $active_users = array( 'online_userlist' => implode($this->user->lang('COMMA_SEPARATOR'), $mchat_users), - 'mchat_users_count' => $this->user->lang('MCHAT_ONLINE_USERS_TOTAL', count($mchat_users)), + 'users_count_title' => $this->user->lang('MCHAT_TITLE_COUNT', count($mchat_users)), + 'users_total' => $this->user->lang('MCHAT_ONLINE_USERS_TOTAL', count($mchat_users)), 'refresh_message' => $this->mchat_format_seconds($this->mchat_session_time()), ); + + /** + * Event to modify collected data about active mChat users + * + * @event dmzx.mchat.active_users_after + * @var array mchat_users Array containing all currently active mChat sessions, mapping from user ID to full username + * @var array active_users Array containing info about currently active mChat users + * @since 2.0.0-RC6 + */ + $vars = array( + 'mchat_users', + 'active_users', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.active_users_after', compact($vars))); + + return $active_users; } /** * Inserts the current user into the mchat_sessions table * - * @return bool + * @return bool Returns true if a new session was created, otherwise false */ public function mchat_add_user_session() { - // Remove expired sessions from the database - $check_time = time() - $this->mchat_session_time(); - $sql = 'DELETE FROM ' . $this->mchat_sessions_table . ' - WHERE user_lastupdate < ' . $check_time; + if (!$this->user->data['is_registered'] || $this->user->data['user_id'] == ANONYMOUS || $this->user->data['is_bot']) + { + return false; + } + + $sql = 'UPDATE ' . $this->mchat_sessions_table . ' + SET user_lastupdate = ' . time() . ' + WHERE user_id = ' . (int) $this->user->data['user_id']; $this->db->sql_query($sql); - $is_new_session = false; + $is_new_session = $this->db->sql_affectedrows() < 1; - if ($this->user->data['user_type'] == USER_FOUNDER || $this->user->data['user_type'] == USER_NORMAL && $this->user->data['user_id'] != ANONYMOUS && !$this->user->data['is_bot']) + if ($is_new_session) { - $sql = 'SELECT * - FROM ' . $this->mchat_sessions_table . ' - WHERE user_id = ' . (int) $this->user->data['user_id']; - $result = $this->db->sql_query($sql); - $row = $this->db->sql_fetchrow($result); - $this->db->sql_freeresult($result); - - if ($row) - { - $sql = 'UPDATE ' . $this->mchat_sessions_table . ' - SET user_lastupdate = ' . time() . ' - WHERE user_id = ' . (int) $this->user->data['user_id']; - } - else - { - $is_new_session = true; - $sql = 'INSERT INTO ' . $this->mchat_sessions_table . ' ' . $this->db->sql_build_array('INSERT', array( - 'user_id' => $this->user->data['user_id'], - 'user_ip' => $this->user->data['user_ip'], - 'user_lastupdate' => time(), - )); - } - + $sql = 'INSERT INTO ' . $this->mchat_sessions_table . ' ' . $this->db->sql_build_array('INSERT', array( + 'user_id' => (int) $this->user->data['user_id'], + 'user_ip' => $this->user->data['user_ip'], + 'user_lastupdate' => time(), + )); $this->db->sql_query($sql); } return $is_new_session; } + /** + * Remove expired sessions from the database + */ + public function mchat_session_gc() + { + $check_time = time() - $this->mchat_session_time(); + + $sql = 'DELETE FROM ' . $this->mchat_sessions_table . ' + WHERE user_lastupdate <= ' . (int) $check_time; + $this->db->sql_query($sql); + } + /** * Prune messages + * + * @return array */ public function mchat_prune() { - if ($this->settings->cfg('mchat_prune')) + $sql_aray = array( + 'SELECT' => 'message_id', + 'FROM' => array($this->mchat_table => 'm'), + ); + + $prune_num = $this->settings->cfg('mchat_prune_num'); + + if (ctype_digit($prune_num)) { - $mchat_total_messages = $this->mchat_total_message_count(); - - if ($mchat_total_messages > $this->settings->cfg('mchat_prune_num')) - { - $sql = 'SELECT message_id - FROM '. $this->mchat_table . ' - ORDER BY message_id ASC'; - $result = $this->db->sql_query_limit($sql, 1); - $first_id = (int) $this->db->sql_fetchfield('message_id'); - $this->db->sql_freeresult($result); - - // Compute new oldest message id - $delete_id = $mchat_total_messages - $this->settings->cfg('mchat_prune_num') + $first_id; - - // Delete older messages - $this->mchat_action('prune', null, $delete_id); - } + // Retain fixed number of messages + $offset = $prune_num; + $sql_aray['ORDER_BY'] = 'message_id DESC'; } + else + { + // Retain messages of a time period + $time_period = strtotime($prune_num, 0); + + if ($time_period === false) + { + $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_MCHAT_TABLE_PRUNE_FAIL', false, array($this->user->data['username'])); + return false; + } + + $offset = 0; + $sql_aray['WHERE'] = 'message_time < ' . (int) (time() - $time_period); + } + + $sql = $this->db->sql_build_query('SELECT', $sql_aray); + $result = $this->db->sql_query_limit($sql, 0, $offset); + $rows = $this->db->sql_fetchrowset(); + $this->db->sql_freeresult($result); + + $prune_ids = array(); + + foreach ($rows as $row) + { + $prune_ids[] = (int) $row['message_id']; + } + + /** + * Event to modify messages that are about to be pruned + * + * @event dmzx.mchat.prune_before + * @var array prune_ids Array of message IDs that are about to be pruned + * @since 2.0.0-RC6 + */ + $vars = array( + 'prune_ids', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.prune_before', compact($vars))); + + if ($prune_ids) + { + $this->db->sql_query('DELETE FROM ' . $this->mchat_table . ' WHERE ' . $this->db->sql_in_set('message_id', $prune_ids)); + $this->db->sql_query('DELETE FROM ' . $this->mchat_log_table . ' WHERE ' . $this->db->sql_in_set('message_id', $prune_ids)); + $this->cache->destroy('sql', $this->mchat_log_table); + } + + $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_MCHAT_TABLE_PRUNED', false, array($this->user->data['username'], count($prune_ids))); + + return $prune_ids; } /** @@ -257,27 +363,77 @@ class functions */ public function mchat_total_message_count() { - return $this->db->get_row_count($this->mchat_table); + $sql_array = array( + 'SELECT' => 'COUNT(*) AS rows_total', + 'FROM' => array($this->mchat_table => 'm'), + ); + + /** + * Event to modifying the SQL query that fetches the total number of mChat messages + * + * @event dmzx.mchat.total_message_count_modify_sql + * @var array sql_array Array with SQL query data to fetch the total message count + * @since 2.0.0-RC6 + */ + $vars = array( + 'sql_array', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.total_message_count_modify_sql', compact($vars))); + + $sql = $this->db->sql_build_query('SELECT', $sql_array); + $result = $this->db->sql_query($sql); + $rows_total = $this->db->sql_fetchfield('rows_total'); + $this->db->sql_freeresult($result); + + return (int) $rows_total; } /** * Fetch messages from the database * - * @param $sql_where + * @param int|array $message_ids IDs of specific messages to fetch, e.g. for fetching edited messages + * @param int $last_id The ID of the latest message that the user has, for fetching new messages * @param int $total * @param int $offset * @return array */ - public function mchat_get_messages($sql_where, $total = 0, $offset = 0) + public function mchat_get_messages($message_ids, $last_id = 0, $total = 0, $offset = 0) { - // Exclude post notifications - if (!$this->settings->cfg('mchat_posts')) + $sql_where_message_id = array(); + + // Fetch new messages + if ($last_id) { - if (!empty($sql_where)) + $sql_where_message_id[] = 'm.message_id > ' . (int) $last_id; + } + + // Fetch edited messages + if ($message_ids) + { + if (!is_array($message_ids)) { - $sql_where = '(' . $sql_where . ') AND '; + $message_ids = array($message_ids); } - $sql_where .= 'm.forum_id = 0'; + + $sql_where_message_id[] = $this->db->sql_in_set('m.message_id', array_map('intval', $message_ids)); + } + + $sql_where_ary = $sql_where_message_id ? array(implode(' OR ', $sql_where_message_id)) : array(); + + if ($this->settings->cfg('mchat_posts')) + { + // If the current user doesn't have permission to see hidden users, exclude their login posts + if (!$this->auth->acl_get('u_viewonline')) + { + $sql_where_ary[] = 'm.post_id <> ' . (int) self::LOGIN_HIDDEN . // Exclude all notifications that were created by hidden users ... + ' OR m.user_id = ' . (int) $this->user->data['user_id'] . // ... but include all login notifications of the current user + ' OR m.forum_id <> 0'; // ... and include all post notifications + } + } + else + { + // Exclude all post notifications + $sql_where_ary[] = 'm.post_id = 0'; } $sql_array = array( @@ -290,13 +446,33 @@ class functions ), array( 'FROM' => array(POSTS_TABLE => 'p'), - 'ON' => 'm.post_id = p.post_id', - ) + 'ON' => 'm.post_id = p.post_id AND m.forum_id <> 0', + ), ), - 'WHERE' => $sql_where, + 'WHERE' => $sql_where_ary ? $this->db->sql_escape('(' . implode(') AND (', $sql_where_ary) . ')') : '', 'ORDER_BY' => 'm.message_id DESC', ); + /** + * Event to modify the SQL query that fetches mChat messages + * + * @event dmzx.mchat.get_messages_modify_sql + * @var array message_ids IDs of specific messages to fetch, e.g. for fetching edited messages + * @var int last_id The ID of the latest message that the user has, for fetching new messages + * @var int total SQL limit + * @var int offset SQL offset + * @var array sql_array Array containing the SQL query data + * @since 2.0.0-RC6 + */ + $vars = array( + 'message_ids', + 'last_id', + 'total', + 'offset', + 'sql_array', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.get_messages_modify_sql', compact($vars))); + $sql = $this->db->sql_build_query('SELECT', $sql_array); $result = $this->db->sql_query_limit($sql, $total, $offset); $rows = $this->db->sql_fetchrowset($result); @@ -314,6 +490,59 @@ class functions return $rows; } + /** + * Fetches log entries from the database and sorts them + * + * @param int $log_id The ID of the latest log entry that the user has + * @return array + */ + public function mchat_get_logs($log_id) + { + $sql_array = array( + 'SELECT' => 'ml.*', + 'FROM' => array($this->mchat_log_table => 'ml'), + 'WHERE' => 'ml.log_id > ' . (int) $log_id, + ); + + $sql = $this->db->sql_build_query('SELECT', $sql_array); + $result = $this->db->sql_query($sql, 3600); + $rows = $this->db->sql_fetchrowset($result); + $this->db->sql_freeresult($result); + + $logs = array( + 'id' => $log_id, + ); + + foreach ($rows as $row) + { + $logs['id'] = max((int) $logs['id'], (int) $row['log_id']); + $logs[] = $row; + } + + return $logs; + } + + /** + * Fetches the highest log ID + * + * @return int + */ + public function get_latest_log_id() + { + $sql_array = array( + 'SELECT' => 'ml.log_id', + 'FROM' => array($this->mchat_log_table => 'ml'), + 'ORDER_BY' => 'log_id DESC', + ); + + $sql = $this->db->sql_build_query('SELECT', $sql_array); + $result = $this->db->sql_query_limit($sql, 1); + $max_log_id = (int) $this->db->sql_fetchfield('log_id'); + $this->db->sql_freeresult($result); + + return $max_log_id; + } + /** * Generates the user legend markup * @@ -323,23 +552,27 @@ class functions { // Grab group details for legend display for who is online on the custom page $order_legend = $this->settings->cfg('legend_sort_groupname') ? 'group_name' : 'group_legend'; + + $sql_array = array( + 'SELECT' => 'g.group_id, g.group_name, g.group_colour, g.group_type', + 'FROM' => array(GROUPS_TABLE => 'g'), + 'WHERE' => 'group_legend <> 0', + 'ORDER_BY' => 'g.' . $order_legend . ' ASC', + ); + if ($this->auth->acl_gets('a_group', 'a_groupadd', 'a_groupdel')) { - $sql = 'SELECT group_id, group_name, group_colour, group_type - FROM ' . GROUPS_TABLE . ' - WHERE group_legend <> 0 - ORDER BY ' . $order_legend . ' ASC'; - } - else - { - $sql = 'SELECT g.group_id, g.group_name, g.group_colour, g.group_type - FROM ' . GROUPS_TABLE . ' g - LEFT JOIN ' . USER_GROUP_TABLE . ' ug ON (g.group_id = ug.group_id AND ug.user_id = ' . $this->user->data['user_id'] . ' AND ug.user_pending = 0) - WHERE g.group_legend <> 0 - AND (g.group_type <> ' . GROUP_HIDDEN . ' - OR ug.user_id = ' . (int) $this->user->data['user_id'] . ') - ORDER BY g.' . $order_legend . ' ASC'; + $sql_array['LEFT_JOIN'] = array( + array( + 'FROM' => array(USER_GROUP_TABLE => 'ug'), + 'ON' => 'g.group_id = ug.group_id AND ug.user_id = ' . (int) $this->user->data['user_id'] . ' AND ug.user_pending = 0', + ), + ); + + $sql_array['WHERE'] .= ' AND (g.group_type <> ' . GROUP_HIDDEN . ' OR ug.user_id = ' . (int) $this->user->data['user_id'] . ')'; } + + $sql = $this->db->sql_build_query('SELECT', $sql_array); $result = $this->db->sql_query($sql); $rows = $this->db->sql_fetchrowset($result); $this->db->sql_freeresult($result); @@ -369,23 +602,61 @@ class functions */ public function mchat_foes() { - if (is_null($this->foes)) - { - $sql = 'SELECT * - FROM ' . ZEBRA_TABLE . ' - WHERE foe = 1 AND user_id = ' . (int) $this->user->data['user_id']; - $result = $this->db->sql_query($sql); - $rows = $this->db->sql_fetchrowset($result); - $this->db->sql_freeresult($result); + $sql = 'SELECT zebra_id + FROM ' . ZEBRA_TABLE . ' + WHERE foe = 1 + AND user_id = ' . (int) $this->user->data['user_id']; + $result = $this->db->sql_query($sql); + $rows = $this->db->sql_fetchrowset($result); + $this->db->sql_freeresult($result); - $this->foes = array(); - foreach ($rows as $row) + $foes = array(); + + foreach ($rows as $row) + { + $foes[] = $row['zebra_id']; + } + + return $foes; + } + + /** + * Fetches post subjects and their forum names + * + * @param array $post_ids + * @return array + */ + public function mchat_get_post_data($post_ids) + { + if (!$post_ids) + { + return array(); + } + + $sql = 'SELECT p.post_id, p.post_subject, f.forum_name + FROM ' . POSTS_TABLE . ' p, ' . FORUMS_TABLE . ' f + WHERE p.forum_id = f.forum_id + AND ' . $this->db->sql_in_set('p.post_id', $post_ids); + + $result = $this->db->sql_query($sql); + $rows = $this->db->sql_fetchrowset($result); + $this->db->sql_freeresult($result); + + $post_subjects = array(); + + foreach ($rows as $row) + { + // Skip deleted posts + if (isset($row['post_subject'])) { - $this->foes[] = $row['zebra_id']; + $post_subjects[$row['post_id']] = array( + 'post_subject' => $row['post_subject'], + 'forum_name' => $row['forum_name'], + ); } } - return $this->foes; + return $post_subjects; } /** @@ -409,43 +680,67 @@ class functions /** * Inserts a message with posting information into the database * - * @param string $mode One of post|quote|edit|reply - * @param $data The post data + * @param string $mode One of post|quote|edit|reply|login + * @param int $forum_id + * @param int $post_id + * @param bool $is_hidden_login */ - public function mchat_insert_posting($mode, $data) + public function mchat_insert_posting($mode, $forum_id, $post_id, $is_hidden_login) { $mode_config = array( 'post' => 'mchat_posts_topic', 'quote' => 'mchat_posts_quote', 'edit' => 'mchat_posts_edit', 'reply' => 'mchat_posts_reply', + 'login' => 'mchat_posts_login', ); - if (empty($mode_config[$mode]) || !$this->settings->cfg($mode_config[$mode])) + $is_mode_enabled = !empty($mode_config[$mode]) && $this->settings->cfg($mode_config[$mode]); + + // Special treatment for login notifications + if ($mode === 'login') { - return; + $forum_id = 0; + $post_id = $is_hidden_login ? self::LOGIN_HIDDEN : self::LOGIN_VISIBLE; } - $board_url = generate_board_url(); - $topic_url = '[url=' . $board_url . '/viewtopic.' . $this->php_ext . '?p=' . $data['post_id'] . '#p' . $data['post_id'] . ']' . $data['post_subject'] . '[/url]'; - $forum_url = '[url=' . $board_url . '/viewforum.' . $this->php_ext . '?f=' . $data['forum_id'] . ']' . $data['forum_name'] . '[/url]'; - $message = $this->user->lang('MCHAT_NEW_' . strtoupper($mode), $topic_url, $forum_url); - - $uid = $bitfield = $options = ''; // will be modified by generate_text_for_storage - generate_text_for_storage($message, $uid, $bitfield, $options, true, false, false); - $sql_ary = array( - 'forum_id' => $data['forum_id'], - 'post_id' => $data['post_id'], - 'user_id' => $this->user->data['user_id'], + $sql_array = array( + 'forum_id' => (int) $forum_id, + 'post_id' => (int) $post_id, + 'user_id' => (int) $this->user->data['user_id'], 'user_ip' => $this->user->data['session_ip'], - 'message' => utf8_normalize_nfc($message), - 'bbcode_bitfield' => $bitfield, - 'bbcode_uid' => $uid, - 'bbcode_options' => $options, + 'message' => 'MCHAT_NEW_' . strtoupper($mode), 'message_time' => time(), ); - $sql = 'INSERT INTO ' . $this->mchat_table . ' ' . $this->db->sql_build_array('INSERT', $sql_ary); - $this->db->sql_query($sql); + + /** + * Event that allows to modify data of a posting notification before it is inserted in the database + * + * @event dmzx.mchat.insert_posting_before + * @var string mode The posting mode, one of post|quote|edit|reply|login + * @var int forum_id The ID of the forum where the post was made, or 0 if mode is login. + * @var int post_id The ID of the post that was made. If mode is login this value is + * one of the constants LOGIN_HIDDEN|LOGIN_VISIBLE + * @var bool is_hidden_login Whether or not the user session is hidden. Only used if mode is login. + * @var array is_mode_enabled Whether or not the posting should be added to the database. + * @var array sql_array An array containing the data that is about to be inserted into the messages table. + * @since 2.0.0-RC6 + */ + $vars = array( + 'mode', + 'forum_id', + 'post_id', + 'is_hidden_login', + 'is_mode_enabled', + 'sql_array', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.insert_posting_before', compact($vars))); + + if ($is_mode_enabled) + { + $sql = 'INSERT INTO ' . $this->mchat_table . ' ' . $this->db->sql_build_array('INSERT', $sql_array); + $this->db->sql_query($sql); + } } /** @@ -479,7 +774,7 @@ class functions */ public function mchat_author_for_message($message_id) { - $sql = 'SELECT u.user_id, u.username, m.message_time + $sql = 'SELECT u.user_id, u.username, m.message_time, m.forum_id, m.post_id FROM ' . $this->mchat_table . ' m LEFT JOIN ' . USERS_TABLE . ' u ON m.user_id = u.user_id WHERE m.message_id = ' . (int) $message_id; @@ -490,92 +785,97 @@ class functions return $row; } - /** - * Returns an array of message IDs that have been deleted from the message table - * - * @param $start_id - * @return array - */ - public function mchat_deleted_ids($start_id) - { - $sql = 'SELECT message_id - FROM ' . $this->mchat_deleted_messages_table . ' - WHERE message_id >= ' . (int) $start_id . ' - ORDER BY message_id DESC'; - $result = $this->db->sql_query($sql, 3600); - $rows = $this->db->sql_fetchrowset(); - $this->db->sql_freeresult($result); - - $missing_ids = array(); - foreach ($rows as $row) - { - $missing_ids[] = (int) $row['message_id']; - } - - return $missing_ids; - } - /** * Performs AJAX actions * - * @param string $action One of add|edit|del|prune + * @param string $action One of add|edit|del * @param array $sql_ary * @param int $message_id * @return bool */ public function mchat_action($action, $sql_ary = null, $message_id = 0) { + $update_session_infos = true; + + /** + * Event to modify the SQL query that adds, edits or deletes an mChat message + * + * @event dmzx.mchat.action_before + * @var string action The action that is being performed, one of add|edit|del + * @var bool sql_ary Array containing SQL data, or null if a message is deleted + * @var int message_id The ID of the message that is being edited or deleted, or 0 if a message is added + * @var bool update_session_infos Whether or not to update the user session + * @since 2.0.0-RC6 + */ + $vars = array( + 'action', + 'sql_ary', + 'message_id', + 'update_session_infos', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.action_before', compact($vars))); + $is_new_session = false; switch ($action) { // User adds a message case 'add': - $this->user->update_session_infos(); + if ($update_session_infos) + { + $this->user->update_session_infos(); + } $is_new_session = $this->mchat_add_user_session(); $this->db->sql_query('INSERT INTO ' . $this->mchat_table . ' ' . $this->db->sql_build_array('INSERT', $sql_ary)); break; // User edits a message case 'edit': - $this->user->update_session_infos(); + if ($update_session_infos) + { + $this->user->update_session_infos(); + } $is_new_session = $this->mchat_add_user_session(); $this->db->sql_query('UPDATE ' . $this->mchat_table . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . ' WHERE message_id = ' . (int) $message_id); + $this->mchat_insert_log('edit', $message_id); $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_EDITED_MCHAT', false, array($this->user->data['username'])); break; // User deletes a message case 'del': - $this->user->update_session_infos(); + if ($update_session_infos) + { + $this->user->update_session_infos(); + } $is_new_session = $this->mchat_add_user_session(); $this->db->sql_query('DELETE FROM ' . $this->mchat_table . ' WHERE message_id = ' . (int) $message_id); - $this->db->sql_query('INSERT INTO ' . $this->mchat_deleted_messages_table . ' ' . $this->db->sql_build_array('INSERT', array('message_id' => (int) $message_id))); - $this->cache->destroy('sql', $this->mchat_deleted_messages_table); + $this->mchat_insert_log('del', $message_id); $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_DELETED_MCHAT', false, array($this->user->data['username'])); break; - - // User triggers messages to be pruned - case 'prune': - $sql = 'SELECT message_id - FROM ' . $this->mchat_table . ' - WHERE message_id < ' . (int) $message_id . ' - ORDER BY message_id DESC'; - $result = $this->db->sql_query($sql); - $rows = $this->db->sql_fetchrowset(); - $this->db->sql_freeresult($result); - - $prune_ids = array(); - foreach ($rows as $row) - { - $prune_ids[] = (int) $row['message_id']; - } - - $this->db->sql_query('DELETE FROM ' . $this->mchat_table . ' WHERE ' .$this->db->sql_in_set('message_id', $prune_ids)); - $this->db->sql_multi_insert($this->mchat_deleted_messages_table, $rows); - $this->cache->destroy('sql', $this->mchat_deleted_messages_table); - break; } return $is_new_session; } + + /** + * @param string $log_type The log type, one of edit|del + * @param int $message_id The ID of the message to which this log entry belongs + * @return int The ID of the newly added log row + */ + public function mchat_insert_log($log_type, $message_id) + { + $this->db->sql_query('INSERT INTO ' . $this->mchat_log_table . ' ' . $this->db->sql_build_array('INSERT', array( + 'log_type' => array_search($log_type, $this->log_types), + 'user_id' => (int) $this->user->data['user_id'], + 'message_id' => (int) $message_id, + 'log_ip' => $this->user->ip, + 'log_time' => time(), + ))); + + $log_id = (int) $this->db->sql_nextid(); + + $this->cache->destroy('sql', $this->mchat_log_table); + + return $log_id; + } } diff --git a/core/mchat.php b/core/mchat.php index 39932d6..2bb6741 100644 --- a/core/mchat.php +++ b/core/mchat.php @@ -11,6 +11,9 @@ namespace dmzx\mchat\core; +use Symfony\Component\HttpFoundation\JsonResponse; +use phpbb\exception\http_exception; + class mchat { /** @var \dmzx\mchat\core\functions */ @@ -55,6 +58,12 @@ class mchat /** @var boolean */ protected $remove_disallowed_bbcodes = false; + /** @var array */ + protected $active_users = null; + + /** @var array */ + protected $foes = null; + /** * Constructor * @@ -129,16 +138,21 @@ class mchat { if (!$this->auth->acl_get('u_mchat_view')) { - throw new \phpbb\exception\http_exception(403, 'NOT_AUTHORISED'); + if (!$this->user->data['is_registered']) + { + login_box(); + } + + throw new http_exception(403, 'NOT_AUTHORISED'); } + $this->user->add_lang_ext('dmzx/mchat', 'mchat'); + if (!$this->settings->cfg('mchat_custom_page')) { - throw new \phpbb\exception\http_exception(403, 'MCHAT_NO_CUSTOM_PAGE'); + throw new http_exception(404, 'MCHAT_NO_CUSTOM_PAGE'); } - $this->functions->mchat_prune(); - $this->functions->mchat_add_user_session(); $this->assign_whois(); @@ -152,7 +166,7 @@ class mchat // Add to navlinks $this->template->assign_block_vars('navlinks', array( 'FORUM_NAME' => $this->user->lang('MCHAT_TITLE'), - 'U_VIEW_FORUM' => $this->helper->route('dmzx_mchat_controller'), + 'U_VIEW_FORUM' => $this->helper->route('dmzx_mchat_page_custom_controller'), )); return $this->helper->render('mchat_body.html', $this->user->lang('MCHAT_TITLE')); @@ -165,12 +179,17 @@ class mchat */ public function page_archive() { + $this->user->add_lang_ext('dmzx/mchat', 'mchat'); + if (!$this->auth->acl_get('u_mchat_view') || !$this->auth->acl_get('u_mchat_archive')) { - throw new \phpbb\exception\http_exception(403, 'MCHAT_NOACCESS_ARCHIVE'); - } + if (!$this->user->data['is_registered']) + { + login_box(); + } - $this->functions->mchat_prune(); + throw new http_exception(403, 'MCHAT_NOACCESS_ARCHIVE'); + } $this->template->assign_var('MCHAT_IS_ARCHIVE_PAGE', true); @@ -180,11 +199,11 @@ class mchat $this->template->assign_block_vars_array('navlinks', array( array( 'FORUM_NAME' => $this->user->lang('MCHAT_TITLE'), - 'U_VIEW_FORUM' => $this->helper->route('dmzx_mchat_controller'), + 'U_VIEW_FORUM' => $this->helper->route('dmzx_mchat_page_custom_controller'), ), array( 'FORUM_NAME' => $this->user->lang('MCHAT_ARCHIVE'), - 'U_VIEW_FORUM' => $this->helper->route('dmzx_mchat_page_controller', array('page' => 'archive')), + 'U_VIEW_FORUM' => $this->helper->route('dmzx_mchat_page_archive_controller'), ), )); @@ -194,21 +213,29 @@ class mchat /** * Controller for mChat IP WHOIS * + * @param string $ip * @return \Symfony\Component\HttpFoundation\Response A Symfony Response object */ - public function page_whois() + public function page_whois($ip) { if (!$this->auth->acl_get('u_mchat_ip')) { - throw new \phpbb\exception\http_exception(403, 'NOT_AUTHORISED'); + if (!$this->user->data['is_registered']) + { + login_box(); + } + + throw new http_exception(403, 'NOT_AUTHORISED'); } + $this->user->add_lang_ext('dmzx/mchat', 'mchat'); + if (!function_exists('user_ipwhois')) { include($this->root_path . 'includes/functions_user.' . $this->php_ext); } - $this->template->assign_var('WHOIS', user_ipwhois($this->request->variable('ip', ''))); + $this->template->assign_var('WHOIS', user_ipwhois($ip)); return $this->helper->render('viewonline_whois.html', $this->user->lang('WHO_IS_ONLINE')); } @@ -222,13 +249,21 @@ class mchat { if (!$this->auth->acl_get('u_mchat_view')) { - throw new \phpbb\exception\http_exception(403, 'NOT_AUTHORISED'); + if (!$this->user->data['is_registered']) + { + login_box(); + } + + throw new http_exception(403, 'NOT_AUTHORISED'); } + $this->user->add_lang_ext('dmzx/mchat', 'mchat'); + $lang_rules = $this->user->lang('MCHAT_RULES_MESSAGE'); + if (!$this->settings->cfg('mchat_rules') && !$lang_rules) { - throw new \phpbb\exception\http_exception(404, 'MCHAT_NO_RULES'); + throw new http_exception(404, 'MCHAT_NO_RULES'); } // If the rules are defined in the language file use them, else just use the entry in the database @@ -241,185 +276,329 @@ class mchat return $this->helper->render('mchat_rules.html', $this->user->lang('MCHAT_RULES')); } + /** + * Initialize AJAX action + * + * @param string $permission Permission that is required to perform the current action + * @param bool $check_form_key + */ + protected function init_action($permission, $check_form_key = true) + { + if (!$this->request->is_ajax() || !$this->auth->acl_get($permission) || ($check_form_key && !check_form_key('mchat', -1))) + { + throw new http_exception(403, 'NO_AUTH_OPERATION'); + } + + // Fix avatars & smilies + if (!defined('PHPBB_USE_BOARD_URL_PATH')) + { + define('PHPBB_USE_BOARD_URL_PATH', true); + } + + $this->user->add_lang_ext('dmzx/mchat', 'mchat'); + } + /** * User submits a message * + * @param bool $return_raw * @return array data sent to client as JSON */ - public function action_add() + public function action_add($return_raw = false) { - if (!$this->auth->acl_get('u_mchat_use') || !check_form_key('mchat', -1)) - { - throw new \phpbb\exception\http_exception(403, 'MCHAT_NOACCESS'); - } + $this->init_action('u_mchat_use'); if ($this->functions->mchat_is_user_flooding()) { - throw new \phpbb\exception\http_exception(400, 'MCHAT_NOACCESS'); + throw new http_exception(400, 'MCHAT_FLOOD'); } $message = $this->request->variable('message', '', true); - if ($this->settings->cfg('mchat_capital_letter')) - { - $message = utf8_ucfirst($message); - } - - $sql_ary = $this->process_message($message, array( + $message_data = array( 'user_id' => $this->user->data['user_id'], 'user_ip' => $this->user->data['session_ip'], 'message_time' => time(), - )); + ); + + /** + * Event to modify a new message before it is inserted in the database + * + * @event dmzx.mchat.action_add_before + * @var string message The message that is about to be processed and added to the database + * @var array message_data Array containing additional information that is added to the database + * @since 2.0.0-RC6 + */ + $vars = array( + 'message', + 'message_data', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.action_add_before', compact($vars))); + + $sql_ary = array_merge($this->process_message($message), $message_data); + + if ($this->settings->cfg('mchat_capital_letter')) + { + $sql_ary['message'] = utf8_ucfirst($sql_ary['message']); + } $is_new_session = $this->functions->mchat_action('add', $sql_ary); - /** - * Event render_helper_add - * - * @event dmzx.mchat.core.render_helper_add - * @since 0.1.2 - */ - $this->dispatcher->dispatch('dmzx.mchat.core.render_helper_add'); - - $data = $this->action_refresh(); + $response = $this->action_refresh(true); if ($is_new_session) { - $data['whois'] = true; + $response = array_merge($response, $this->action_whois(true)); } - return $data; + /** + * Event to modify message data of a user's new message before it is sent back to the user + * + * @event dmzx.mchat.action_add_after + * @var string message The message that was added to the database + * @var array message_data Array containing additional information that was added to the database + * @var bool is_new_session Indicating whether the message triggered a new mChat session to be created for the user + * @var array response The data that is sent back to the user + * @var boolean return_raw Whether to return a raw array or a JsonResponse object + * @since 2.0.0-RC6 + */ + $vars = array( + 'message', + 'message_data', + 'is_new_session', + 'response', + 'return_raw', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.action_add_after', compact($vars))); + + return $return_raw ? $response : new JsonResponse($response); } /** * User edits a message * + * @param bool $return_raw * @return array data sent to client as JSON */ - public function action_edit() + public function action_edit($return_raw = false) { + $this->init_action('u_mchat_use'); + $message_id = $this->request->variable('message_id', 0); - if (!$message_id || !check_form_key('mchat', -1)) + if (!$message_id) { - throw new \phpbb\exception\http_exception(403, 'MCHAT_NOACCESS'); + throw new http_exception(403, 'NO_AUTH_OPERATION'); } $author = $this->functions->mchat_author_for_message($message_id); - if (!$author || !$this->auth_message('u_mchat_edit', $author['user_id'], $author['message_time'])) + if (!$author || $author['post_id'] || !$this->auth_message('edit', $author['user_id'], $author['message_time'])) { - throw new \phpbb\exception\http_exception(403, 'MCHAT_NOACCESS'); + throw new http_exception(403, 'NO_AUTH_OPERATION'); } $this->template->assign_var('MCHAT_IS_ARCHIVE_PAGE', $this->request->variable('archive', false)); $message = $this->request->variable('message', '', true); - - $sql_ary = $this->process_message($message, array( - 'edit_time' => time(), - )); - + $sql_ary = $this->process_message($message); $this->functions->mchat_action('edit', $sql_ary, $message_id); - /** - * Event render_helper_edit - * - * @event dmzx.mchat.core.render_helper_edit - * @since 0.1.4 - */ - $this->dispatcher->dispatch('dmzx.mchat.core.render_helper_edit'); - - $sql_where = 'm.message_id = ' . (int) $message_id; - $rows = $this->functions->mchat_get_messages($sql_where, 1); + $rows = $this->functions->mchat_get_messages($message_id); $this->assign_global_template_data(); $this->assign_messages($rows); - return array('edit' => $this->render_template('mchat_messages.html')); + $response = array('edit' => $this->render_template('mchat_messages.html')); + + /** + * Event to modify the data of an edited message + * + * @event dmzx.mchat.action_edit_after + * @var int message_id The ID of the edited message + * @var string message The content of the edited message that was added to the database + * @var array author Information about the message author + * @var array response The data that is sent back to the user + * @var boolean return_raw Whether to return a raw array or a JsonResponse object + * @since 2.0.0-RC6 + */ + $vars = array( + 'message_id', + 'message', + 'author', + 'response', + 'return_raw', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.action_edit_after', compact($vars))); + + return $return_raw ? $response : new JsonResponse($response); } /** * User deletes a message * + * @param bool $return_raw * @return array data sent to client as JSON */ - public function action_del() + public function action_del($return_raw = false) { + $this->init_action('u_mchat_use'); + $message_id = $this->request->variable('message_id', 0); - if (!$message_id || !check_form_key('mchat', -1)) + if (!$message_id) { - throw new \phpbb\exception\http_exception(403, 'MCHAT_NOACCESS'); + throw new http_exception(403, 'NO_AUTH_OPERATION'); } $author = $this->functions->mchat_author_for_message($message_id); - if (!$author || !$this->auth_message('u_mchat_delete', $author['user_id'], $author['message_time'])) + if (!$author || !$this->auth_message('delete', $author['user_id'], $author['message_time'])) { - throw new \phpbb\exception\http_exception(403, 'MCHAT_NOACCESS'); + throw new http_exception(403, 'NO_AUTH_OPERATION'); } - /** - * Event render_helper_delete - * - * @event dmzx.mchat.core.render_helper_delete - * @since 0.1.4 - */ - $this->dispatcher->dispatch('dmzx.mchat.core.render_helper_delete'); - $this->functions->mchat_action('del', null, $message_id); - return array('del' => true); + $response = array('del' => true); + + /** + * Event that is triggered after an mChat message was deleted + * + * @event dmzx.mchat.action_delete_after + * @var int message_id The ID of the deleted message + * @var array author Information about the message author + * @var array response The data that is sent back to the user + * @var boolean return_raw Whether to return a raw array or a JsonResponse object + * @since 2.0.0-RC6 + */ + $vars = array( + 'message_id', + 'author', + 'response', + 'return_raw', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.action_delete_after', compact($vars))); + + return $return_raw ? $response : new JsonResponse($response); } /** * User checks for new messages * + * @param bool $return_raw * @return array sent to client as JSON */ - public function action_refresh() + public function action_refresh($return_raw = false) { - if (!$this->auth->acl_get('u_mchat_view')) - { - throw new \phpbb\exception\http_exception(403, 'MCHAT_NOACCESS'); - } + $this->init_action('u_mchat_view', false); - // Keep the session alive forever if there is no user session timeout - if (!$this->settings->cfg('mchat_timeout')) + // Keep the session alive forever if there is no session timeout + $keep_session_alive = !$this->settings->cfg('mchat_timeout'); + + // Whether to check the log table for new entries + $need_log_update = $this->settings->cfg('mchat_live_updates'); + + /** + * Event that is triggered before new mChat messages are checked + * + * @event dmzx.mchat.action_refresh_before + * @var bool keep_session_alive Whether to the user's phpBB session + * @var bool need_log_update Whether to check the log table for new entries + * @since 2.0.0-RC6 + */ + $vars = array( + 'keep_session_alive', + 'need_log_update', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.action_refresh_before', compact($vars))); + + if ($keep_session_alive) { $this->user->update_session_infos(); } - $message_first_id = $this->request->variable('message_first_id', 0); - $message_last_id = $this->request->variable('message_last_id', 0); - $message_edits = $this->request->variable('message_edits', array(0)); + $response = array('refresh' => true); + $log_edit_del_ids = array( + 'edit' => array(), + 'del' => array(), + ); - // Request new messages - $sql_where = 'm.message_id > ' . (int) $message_last_id; - - // Request edited messages - if ($this->settings->cfg('mchat_live_updates') && $message_last_id > 0) + if ($need_log_update) { - $sql_where .= sprintf(' OR (m.message_id BETWEEN %d AND %d AND m.edit_time > 0)', (int) $message_first_id , (int) $message_last_id); - if ($this->settings->cfg('mchat_edit_delete_limit')) + $log_id = $this->request->variable('log', 0); + $log_rows = $this->functions->mchat_get_logs($log_id); + + $response['log'] = $log_rows['id']; + unset($log_rows['id']); + + $edit_delete_limit = $this->settings->cfg('mchat_edit_delete_limit'); + $time_limit = $edit_delete_limit ? time() - $edit_delete_limit : 0; + + foreach ($log_rows as $log_row) { - $sql_where .= sprintf(' AND m.message_time > %d', time() - $this->settings->cfg('mchat_edit_delete_limit')); + $log_type = $log_row['log_type']; + + if (isset($this->functions->log_types[$log_type])) + { + if ($log_row['user_id'] != $this->user->data['user_id'] && $log_row['log_time'] > $time_limit) + { + $log_type_name = $this->functions->log_types[$log_type]; + $log_edit_del_ids[$log_type_name][] = (int) $log_row['message_id']; + } + } + + /** + * Event that allows processing log messages + * + * @event dmzx.mchat.action_refresh_process_log_row + * @var array response The data that is sent back to the user (still incomplete at this point) + * @var array log_row The log data + * @since 2.0.0-RC6 + */ + $vars = array( + 'response', + 'log_row', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.action_refresh_process_log_row', compact($vars))); } } - $rows = $this->functions->mchat_get_messages($sql_where); + $last_id = $this->request->variable('last', 0); + $total = 0; + $offset = 0; + + /** + * Event that allows modifying data before new mChat messages are fetched + * + * @event dmzx.mchat.action_refresh_get_messages_before + * @var array response The data that is sent back to the user (still incomplete at this point) + * @var array log_edit_del_ids An array containing IDs of messages that have been edited or deleted since the user's last refresh + * @var int last_id The latest message that the user has + * @var int total Limit the number of messages to fetch + * @var int offset The number of messages to skip + * @since 2.0.0-RC6 + */ + $vars = array( + 'response', + 'log_edit_del_ids', + 'last_id', + 'total', + 'offset', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.action_refresh_get_messages_before', compact($vars))); + + $rows = $this->functions->mchat_get_messages($log_edit_del_ids['edit'], $last_id, $total, $offset); $rows_refresh = array(); $rows_edit = array(); foreach ($rows as $row) { - $message_id = $row['message_id']; - if ($message_id > $message_last_id) + if ($row['message_id'] > $last_id) { $rows_refresh[] = $row; } - else if (!isset($message_edits[$message_id]) || $message_edits[$message_id] < $row['edit_time']) + else if (in_array($row['message_id'], $log_edit_del_ids['edit'])) { $rows_edit[] = $row; } @@ -430,8 +609,6 @@ class mchat $this->assign_global_template_data(); } - $response = array('refresh' => true); - // Assign new messages if ($rows_refresh) { @@ -447,28 +624,65 @@ class mchat } // Assign deleted messages - if ($this->settings->cfg('mchat_live_updates') && $message_last_id > 0) + if ($log_edit_del_ids['del']) { - $deleted_message_ids = $this->functions->mchat_deleted_ids($message_first_id); - if ($deleted_message_ids) - { - $response['del'] = $deleted_message_ids; - } + $response['del'] = $log_edit_del_ids['del']; } - return $response; + /** + * Event to modify the data that is sent to the user after checking for new mChat message + * + * @event dmzx.mchat.action_refresh_after + * @var array rows The rows that where fetched from the database + * @var array response The data that is sent back to the user + * @var boolean return_raw Whether to return a raw array or a JsonResponse object + * @since 2.0.0-RC6 + */ + $vars = array( + 'rows', + 'response', + 'return_raw', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.action_refresh_after', compact($vars))); + + return $return_raw ? $response : new JsonResponse($response); } /** * User requests who is chatting * + * @param bool $return_raw * @return array data sent to client as JSON */ - public function action_whois() + public function action_whois($return_raw = false) { + $this->init_action('u_mchat_view', false); + $this->assign_whois(); - return array('whois' => $this->render_template('mchat_whois.html')); + $response = array('whois' => $this->render_template('mchat_whois.html')); + + if ($this->settings->cfg('mchat_navbar_link_count') && $this->settings->cfg('mchat_navbar_link') && $this->settings->cfg('mchat_custom_page') && $this->active_users) + { + $response['navlink'] = $this->active_users['users_count_title']; + $response['navlink_title'] = strip_tags($this->active_users['users_total']); + } + + /** + * Event to modify the result of the Who Is Online update + * + * @event dmzx.mchat.action_whois_after + * @var array response The data that is sent back to the user + * @var boolean return_raw Whether to return a raw array or a JsonResponse object + * @since 2.0.0-RC6 + */ + $vars = array( + 'response', + 'return_raw', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.action_whois_after', compact($vars))); + + return $return_raw ? $response : new JsonResponse($response); } /** @@ -476,12 +690,34 @@ class mchat */ public function render_page_header_link() { - $this->template->assign_vars(array( - 'MCHAT_ALLOW_VIEW' => $this->auth->acl_get('u_mchat_view'), - 'MCHAT_NAVBAR_LINK' => $this->settings->cfg('mchat_navbar_link'), - 'MCHAT_CUSTOM_PAGE' => $this->settings->cfg('mchat_custom_page'), - 'U_MCHAT' => $this->helper->route('dmzx_mchat_controller'), - )); + if (!$this->auth->acl_get('u_mchat_view')) + { + return; + } + + $navbar_link = $this->settings->cfg('mchat_navbar_link'); + $custom_page = $this->settings->cfg('mchat_custom_page'); + + $template_data = array( + 'MCHAT_NAVBAR_LINK' => $navbar_link, + 'MCHAT_CUSTOM_PAGE' => $custom_page, + 'MCHAT_TITLE' => $this->user->lang('MCHAT_TITLE'), + 'MCHAT_TITLE_HINT' => $this->user->lang('MCHAT_TITLE'), + 'U_MCHAT' => $this->helper->route('dmzx_mchat_page_custom_controller'), + ); + + if ($navbar_link && $custom_page && $this->settings->cfg('mchat_navbar_link_count')) + { + if ($this->active_users === null) + { + $this->active_users = $this->functions->mchat_active_users(); + } + + $template_data['MCHAT_TITLE'] = $this->active_users['users_count_title']; + $template_data['MCHAT_TITLE_HINT'] = strip_tags($this->active_users['users_total']); + } + + $this->template->assign_vars($template_data); } /** @@ -491,6 +727,18 @@ class mchat */ protected function render_page($page) { + /** + * Event that is triggered before mChat is rendered + * + * @event dmzx.mchat.render_page_before + * @var string page The page that is rendered, one of index|custom|archive + * @since 2.0.0-RC6 + */ + $vars = array( + 'page', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.render_page_before', compact($vars))); + // Add lang file $this->user->add_lang('posting'); @@ -498,11 +746,8 @@ class mchat $lang_static_message = $this->user->lang('MCHAT_STATIC_MESSAGE'); $static_message = $lang_static_message ?: $this->settings->cfg('mchat_static_message'); - $u_mchat_use = $this->auth->acl_get('u_mchat_use'); - $this->template->assign_vars(array( - 'MCHAT_ALLOW_USE' => $u_mchat_use, - 'S_BBCODE_ALLOWED' => $this->settings->cfg('allow_bbcode') && $this->auth->acl_get('u_mchat_bbcode'), + 'MCHAT_PAGE' => $page, 'MCHAT_ALLOW_SMILES' => $this->settings->cfg('allow_smilies') && $this->auth->acl_get('u_mchat_smilies'), 'MCHAT_INPUT_AREA' => $this->settings->cfg('mchat_input_area'), 'MCHAT_MESSAGE_TOP' => $this->settings->cfg('mchat_message_top'), @@ -517,18 +762,20 @@ class mchat 'MCHAT_PAUSE_ON_INPUT' => $this->settings->cfg('mchat_pause_on_input'), 'MCHAT_MESSAGE_LNGTH' => $this->settings->cfg('mchat_max_message_lngth'), 'MCHAT_WHOIS_INDEX' => $this->settings->cfg('mchat_whois_index'), - 'MCHAT_WHOIS_REFRESH' => $this->settings->cfg('mchat_whois') ? $this->settings->cfg('mchat_whois_refresh') * 1000 : 0, + 'MCHAT_WHOIS_REFRESH' => $this->settings->cfg('mchat_whois_index') || $this->settings->cfg('mchat_stats_index') ? $this->settings->cfg('mchat_whois_refresh') * 1000 : 0, 'MCHAT_REFRESH_JS' => $this->settings->cfg('mchat_refresh') * 1000, 'MCHAT_ARCHIVE' => $this->auth->acl_get('u_mchat_archive'), 'MCHAT_RULES' => $this->user->lang('MCHAT_RULES_MESSAGE') || $this->settings->cfg('mchat_rules'), 'MCHAT_WHOIS_REFRESH_EXPLAIN' => $this->user->lang('MCHAT_WHO_IS_REFRESH_EXPLAIN', $this->settings->cfg('mchat_whois_refresh')), 'MCHAT_SESSION_TIMELEFT' => $this->user->lang('MCHAT_SESSION_ENDS', gmdate('H:i:s', (int) $this->settings->cfg('mchat_timeout'))), + 'MCHAT_LOG_ID' => $this->functions->get_latest_log_id(), 'MCHAT_STATIC_MESS' => htmlspecialchars_decode($static_message), 'A_MCHAT_MESS_LONG' => addslashes($this->user->lang('MCHAT_MESS_LONG', $this->settings->cfg('mchat_max_message_lngth'))), 'A_MCHAT_REFRESH_YES' => addslashes($this->user->lang('MCHAT_REFRESH_YES', $this->settings->cfg('mchat_refresh'))), - 'U_MCHAT_CUSTOM_PAGE' => $this->helper->route('dmzx_mchat_controller'), - 'U_MCHAT_RULES' => $this->helper->route('dmzx_mchat_page_controller', array('page' => 'rules')), - 'U_MCHAT_ARCHIVE_URL' => $this->helper->route('dmzx_mchat_page_controller', array('page' => 'archive')), + 'A_COOKIE_NAME' => addslashes($this->settings->cfg('cookie_name', true) . '_'), + 'U_MCHAT_CUSTOM_PAGE' => $this->helper->route('dmzx_mchat_page_custom_controller'), + 'U_MCHAT_RULES' => $this->helper->route('dmzx_mchat_page_rules_controller'), + 'U_MCHAT_ARCHIVE_URL' => $this->helper->route('dmzx_mchat_page_archive_controller'), )); // The template needs some language variables if we display relative time for messages @@ -547,25 +794,25 @@ class mchat // Get actions which the user is allowed to perform on the current page $actions = array_keys(array_filter(array( - 'edit' => $this->auth_message('u_mchat_edit', true, time()), - 'del' => $this->auth_message('u_mchat_delete', true, time()), + 'edit' => $this->auth_message('edit', true, time()), + 'del' => $this->auth_message('delete', true, time()), 'refresh' => $page !== 'archive' && $this->auth->acl_get('u_mchat_view'), - 'add' => $page !== 'archive' && $u_mchat_use, - 'whois' => $page !== 'archive' && $this->settings->cfg('mchat_whois'), + 'add' => $page !== 'archive' && $this->auth->acl_get('u_mchat_use'), + 'whois' => $page !== 'archive' && ($this->settings->cfg('mchat_whois_index') || $this->settings->cfg('mchat_stats_index')), ))); foreach ($actions as $i => $action) { $this->template->assign_block_vars('mchaturl', array( 'ACTION' => $action, - 'URL' => $this->helper->route('dmzx_mchat_action_controller', array('action' => $action)), + 'URL' => $this->helper->route('dmzx_mchat_action_' . $action . '_controller'), 'IS_LAST' => $i + 1 === count($actions), )); } $limit = $this->settings->cfg('mchat_message_num_' . $page); $start = $page === 'archive' ? $this->request->variable('start', 0) : 0; - $rows = $this->functions->mchat_get_messages('', $limit, $start); + $rows = $this->functions->mchat_get_messages(array(), 0, $limit, $start); $this->assign_global_template_data(); $this->assign_messages($rows); @@ -573,14 +820,33 @@ class mchat // Render pagination if ($page === 'archive') { - $archive_url = $this->helper->route('dmzx_mchat_page_controller', array('page' => 'archive')); + $archive_url = $this->helper->route('dmzx_mchat_page_archive_controller'); $total_messages = $this->functions->mchat_total_message_count(); + + /** + * Event to modify mChat pagination on the archive page + * + * @event dmzx.mchat.render_page_pagination_before + * @var string archive_url Pagination base URL + * @var int total_messages Total number of messages + * @var int limit Number of messages to display per page + * @var int start The message which should be considered currently active, used to determine the page we're on + * @since 2.0.0-RC6 + */ + $vars = array( + 'archive_url', + 'total_messages', + 'limit', + 'start', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.render_page_pagination_before', compact($vars))); + $this->pagination->generate_template_pagination($archive_url, 'pagination', 'start', $total_messages, $limit, $start); $this->template->assign_var('MCHAT_TOTAL_MESSAGES', $this->user->lang('MCHAT_TOTALMESSAGES', $total_messages)); } // Render legend - if ($page !== 'index' && $this->settings->cfg('mchat_whois')) + if ($page !== 'index') { $legend = $this->functions->mchat_legend(); $this->template->assign_var('LEGEND', implode($this->user->lang('COMMA_SEPARATOR'), $legend)); @@ -593,7 +859,7 @@ class mchat $this->template->assign_vars(array( 'MCHAT_IS_COLLAPSIBLE' => true, 'S_MCHAT_HIDDEN' => in_array($cc_fid, $this->cc_operator->get_user_categories()), - 'U_MCHAT_COLLAPSE_URL' => $this->helper->route('phpbb_collapsiblecategories_main_controller', array( + 'U_MCHAT_COLLAPSE_URL' => $this->helper->route('phpbb_collapsiblecategories_main_controller', array( 'forum_id' => $cc_fid, 'hash' => generate_link_hash('collapsible_' . $cc_fid), )), @@ -602,18 +868,24 @@ class mchat $this->assign_authors(); - if ($u_mchat_use) + if ($this->auth->acl_get('u_mchat_use')) { add_form_key('mchat'); } /** - * Event render_helper_aft - * - * @event dmzx.mchat.core.render_helper_aft - * @since 0.1.2 - */ - $this->dispatcher->dispatch('dmzx.mchat.core.render_helper_aft'); + * Event that is triggered after mChat was rendered + * + * @event dmzx.mchat.render_page_after + * @var string page The page that was rendered, one of index|custom|archive + * @var array actions Array containing URLs to actions the user is allowed to perform + * @since 2.0.0-RC6 + */ + $vars = array( + 'page', + 'actions', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.render_page_after', compact($vars))); } /** @@ -627,7 +899,7 @@ class mchat $author_names = array(); $author_homepages = array(); - foreach ($meta['authors'] as $author) + foreach (array_slice($meta['authors'], 0, 2) as $author) { $author_names[] = $author['name']; $author_homepages[] = sprintf('%2$s', $author['homepage'], $author['name']); @@ -643,22 +915,38 @@ class mchat /** * Assigns common template data that is required for displaying messages */ - protected function assign_global_template_data() + public function assign_global_template_data() { - $this->template->assign_vars(array( - 'MCHAT_ALLOW_IP' => $this->auth->acl_get('u_mchat_ip'), - 'MCHAT_ALLOW_PM' => $this->auth->acl_get('u_mchat_pm'), - 'MCHAT_ALLOW_LIKE' => $this->auth->acl_get('u_mchat_like'), - 'MCHAT_ALLOW_QUOTE' => $this->auth->acl_get('u_mchat_quote'), - 'MCHAT_ALLOW_PERMISSIONS' => $this->auth->acl_get('a_authusers'), - 'MCHAT_EDIT_DELETE_LIMIT' => 1000 * $this->settings->cfg('mchat_edit_delete_limit'), - 'MCHAT_EDIT_DELETE_IGNORE' => $this->settings->cfg('mchat_edit_delete_limit') && $this->auth->acl_get('m_'), - 'MCHAT_RELATIVE_TIME' => $this->settings->cfg('mchat_relative_time'), - 'MCHAT_USER_TIMEOUT' => 1000 * $this->settings->cfg('mchat_timeout'), - 'S_MCHAT_AVATARS' => $this->display_avatars(), - 'EXT_URL' => generate_board_url() . '/ext/dmzx/mchat/', - 'STYLE_PATH' => generate_board_url() . '/styles/' . rawurlencode($this->user->style['style_path']), - )); + $template_data = array( + 'S_BBCODE_ALLOWED' => $this->auth->acl_get('u_mchat_bbcode') && $this->settings->cfg('allow_bbcode'), + 'MCHAT_ALLOW_USE' => $this->auth->acl_get('u_mchat_use'), + 'MCHAT_ALLOW_IP' => $this->auth->acl_get('u_mchat_ip'), + 'MCHAT_ALLOW_PM' => $this->auth->acl_get('u_mchat_pm'), + 'MCHAT_ALLOW_LIKE' => $this->auth->acl_get('u_mchat_like'), + 'MCHAT_ALLOW_QUOTE' => $this->auth->acl_get('u_mchat_quote'), + 'MCHAT_ALLOW_PERMISSIONS' => $this->auth->acl_get('a_authusers'), + 'MCHAT_EDIT_DELETE_LIMIT' => 1000 * $this->settings->cfg('mchat_edit_delete_limit'), + 'MCHAT_EDIT_DELETE_IGNORE' => $this->settings->cfg('mchat_edit_delete_limit') && ($this->auth->acl_get('u_mchat_moderator_edit') || $this->auth->acl_get('u_mchat_moderator_delete')), + 'MCHAT_RELATIVE_TIME' => $this->settings->cfg('mchat_relative_time'), + 'MCHAT_TIMEOUT' => 1000 * $this->settings->cfg('mchat_timeout'), + 'S_MCHAT_AVATARS' => $this->display_avatars(), + 'EXT_URL' => generate_board_url() . '/ext/dmzx/mchat/', + 'STYLE_PATH' => generate_board_url() . '/styles/' . rawurlencode($this->user->style['style_path']), + ); + + /** + * Event that allows adding global templte data for mChat + * + * @event dmzx.mchat.global_modify_template_data + * @var array template_data The data that is about to be assigned to the template + * @since 2.0.0-RC6 + */ + $vars = array( + 'template_data', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.global_modify_template_data', compact($vars))); + + $this->template->assign_vars($template_data); } /** @@ -676,26 +964,9 @@ class mchat * * @param array $rows */ - protected function assign_messages($rows) + public function assign_messages($rows) { - // Auth checks - foreach ($rows as $i => $row) - { - if ($row['forum_id']) - { - // No permission to read forum - if (!$this->auth->acl_get('f_read', $row['forum_id'])) - { - unset($rows[$i]); - } - - // Post is not approved and no approval permission - if ($row['post_visibility'] != ITEM_APPROVED && !$this->auth->acl_get('m_approve', $row['forum_id'])) - { - unset($rows[$i]); - } - } - } + $rows = array_filter($rows, array($this, 'has_read_auth')); if (!$rows) { @@ -708,13 +979,18 @@ class mchat $rows = array_reverse($rows); } - $foes = $this->functions->mchat_foes(); + if ($this->foes === null) + { + $this->foes = $this->functions->mchat_foes(); + } + // Remove template data from previous render $this->template->destroy_block_vars('mchatrow'); $user_avatars = array(); - foreach ($rows as $i => $row) + // Cache avatars + foreach ($rows as $row) { if (!isset($user_avatars[$row['user_id']])) { @@ -730,10 +1006,10 @@ class mchat $board_url = generate_board_url() . '/'; - foreach ($rows as $i => $row) - { - $message_for_edit = generate_text_for_edit($row['message'], $row['bbcode_uid'], $row['bbcode_options']); + $this->process_notifications($rows, $board_url); + foreach ($rows as $row) + { $username_full = get_username_string('full', $row['user_id'], $row['username'], $row['user_colour'], $this->user->lang('GUEST')); // Fix profile link root path by replacing relative paths with absolute board URL @@ -742,46 +1018,198 @@ class mchat $username_full = preg_replace('#(?<=href=")[\./]+?/(?=\w)#', $board_url, $username_full); } - if (in_array($row['user_id'], $foes)) + if (in_array($row['user_id'], $this->foes)) { $row['message'] = $this->user->lang('MCHAT_FOE', $username_full); } $message_age = time() - $row['message_time']; $minutes_ago = $this->get_minutes_ago($message_age); - $datetime = $this->user->format_date($row['message_time'], $this->settings->cfg('mchat_date')); + $datetime = $this->user->format_date($row['message_time'], $this->settings->cfg('mchat_date'), true); $is_poster = $row['user_id'] != ANONYMOUS && $this->user->data['user_id'] == $row['user_id']; - $this->template->assign_block_vars('mchatrow', array( - 'MCHAT_ALLOW_EDIT' => $this->auth_message('u_mchat_edit', $row['user_id'], $row['message_time']), - 'MCHAT_ALLOW_DEL' => $this->auth_message('u_mchat_delete', $row['user_id'], $row['message_time']), + $message_for_edit = generate_text_for_edit($row['message'], $row['bbcode_uid'], $row['bbcode_options']); + + $template_data = array( + 'MCHAT_ALLOW_EDIT' => $this->auth_message('edit', $row['user_id'], $row['message_time']), + 'MCHAT_ALLOW_DEL' => $this->auth_message('delete', $row['user_id'], $row['message_time']), 'MCHAT_USER_AVATAR' => $user_avatars[$row['user_id']], 'U_VIEWPROFILE' => $row['user_id'] != ANONYMOUS ? append_sid("{$board_url}{$this->root_path}memberlist.{$this->php_ext}", 'mode=viewprofile&u=' . $row['user_id']) : '', 'MCHAT_IS_POSTER' => $is_poster, - 'MCHAT_PM' => !$is_poster && $this->settings->cfg('allow_privmsg') && $this->auth->acl_get('u_sendpm') && ($row['user_allow_pm'] || $this->auth->acl_gets('a_', 'm_') || $this->auth->acl_getf_global('m_')) ? append_sid("{$board_url}{$this->root_path}ucp.{$this->php_ext}", 'i=pm&mode=compose&u=' . $row['user_id']) : '', + 'MCHAT_IS_NOTIFICATION' => (bool) $row['post_id'], + 'MCHAT_PM' => !$is_poster && $this->settings->cfg('allow_privmsg') && $this->auth->acl_get('u_sendpm') && ($row['user_allow_pm'] || $this->auth->acl_gets('a_', 'm_') || $this->auth->acl_getf_global('m_')) ? append_sid("{$board_url}{$this->root_path}ucp.{$this->php_ext}", 'i=pm&mode=compose&mchat_pm_quote_message=' . (int) $row['message_id'] . '&u=' . $row['user_id']) : '', 'MCHAT_MESSAGE_EDIT' => $message_for_edit['text'], 'MCHAT_MESSAGE_ID' => $row['message_id'], 'MCHAT_USERNAME_FULL' => $username_full, 'MCHAT_USERNAME' => get_username_string('username', $row['user_id'], $row['username'], $row['user_colour'], $this->user->lang('GUEST')), 'MCHAT_USERNAME_COLOR' => get_username_string('colour', $row['user_id'], $row['username'], $row['user_colour'], $this->user->lang('GUEST')), 'MCHAT_WHOIS_USER' => $this->user->lang('MCHAT_WHOIS_USER', $row['user_ip']), - 'MCHAT_U_IP' => $this->helper->route('dmzx_mchat_page_controller', array('page' => 'whois', 'ip' => $row['user_ip'])), - 'MCHAT_U_PERMISSIONS' => append_sid("{$board_url}{$this->root_path}adm/index.{$this->php_ext}" ,'i=permissions&mode=setting_user_global&user_id[0]=' . $row['user_id'], true, $this->user->session_id), + 'MCHAT_U_IP' => $this->helper->route('dmzx_mchat_page_whois_controller', array('ip' => $row['user_ip'])), + 'MCHAT_U_PERMISSIONS' => append_sid("{$board_url}{$this->root_path}adm/index.{$this->php_ext}", 'i=permissions&mode=setting_user_global&user_id%5B0%5D=' . $row['user_id'], true, $this->user->session_id), 'MCHAT_MESSAGE' => generate_text_for_display($row['message'], $row['bbcode_uid'], $row['bbcode_bitfield'], $row['bbcode_options']), 'MCHAT_TIME' => $minutes_ago === -1 ? $datetime : $this->user->lang('MCHAT_MINUTES_AGO', $minutes_ago), 'MCHAT_DATETIME' => $datetime, 'MCHAT_MINUTES_AGO' => $minutes_ago, 'MCHAT_RELATIVE_UPDATE' => 60 - $message_age % 60, 'MCHAT_MESSAGE_TIME' => $row['message_time'], - 'MCHAT_EDIT_TIME' => $row['edit_time'], - )); + ); + + /** + * Event to modify the template data of an mChat message before it is sent to the template + * + * @event dmzx.mchat.message_modify_template_data + * @var array template_data The data that is about to be assigned to the template + * @var string username_full The link to the user profile, e.g. Username + * @var bool is_notification Whether or not this message is a notification + * @var array row The raw message data as fetched from the database + * @var int message_age The number of seconds that have passed since the message was posted + * @var int minutes_ago The number of minutes that have passed since the message was posted, or -1 + * @var string datetime The full date in the user-specific date format + * @var bool is_poster Whether or not the current user posted this message + * @var array message_for_edit The data for editing the message + * @since 2.0.0-RC6 + */ + $vars = array( + 'template_data', + 'username_full', + 'is_notification', + 'row', + 'message_age', + 'minutes_ago', + 'datetime', + 'is_poster', + 'message_for_edit', + ); + extract($this->dispatcher->trigger_event('dmzx.mchat.message_modify_template_data', compact($vars))); + + $this->template->assign_block_vars('mchatrow', $template_data); } } /** - * Calculates the number of minutes that have passed since the message was posted. If relative time is disabled - * or the message is older than 59 minutes or we render for the archive, -1 is returned. + * Returns true of the user is allowed to read the given message row + * + * @param array $row + * @return bool + */ + protected function has_read_auth($row) + { + if ($row['forum_id']) + { + // No permission to read forum + if (!$this->auth->acl_get('f_read', $row['forum_id'])) + { + return false; + } + + // Post is not approved and no approval permission + if ($row['post_visibility'] != ITEM_APPROVED && !$this->auth->acl_get('m_approve', $row['forum_id'])) + { + return false; + } + } + + return true; + } + + /** + * Checks the post rows for notifications and converts their language keys + * + * @param array $rows The rows to modify + * @param string $board_url + */ + protected function process_notifications(&$rows, $board_url) + { + $notification_post_ids = array(); + + // All language keys of valid notifications. We need to check for them here because + // notifications in < 2.0.0-RC6 are plain text and don't need to be processed here. + $notification_lang = array( + 'MCHAT_NEW_POST', + 'MCHAT_NEW_QUOTE', + 'MCHAT_NEW_EDIT', + 'MCHAT_NEW_REPLY', + 'MCHAT_NEW_LOGIN', + ); + + foreach ($rows as $i => $row) + { + // If post_id is 0 it's not a notification. + if ($row['post_id'] && in_array($row['message'], $notification_lang)) + { + if ($row['forum_id']) + { + $notification_post_ids[] = $row['post_id']; + } + else + { + $rows[$i] = $this->process_notification($row, $board_url); + } + } + } + + $notification_post_data = $this->functions->mchat_get_post_data($notification_post_ids); + + if ($notification_post_data) + { + foreach ($rows as $i => $row) + { + if (in_array($row['post_id'], $notification_post_ids)) + { + $rows[$i] = $this->process_notification($row, $board_url, $notification_post_data[$row['post_id']]); + } + } + } + } + + /** + * Converts the message field of the post row so that it can be passed to generate_text_for_display() + * + * @param array $row + * @param string $board_url + * @param array $post_data + * @return array + */ + protected function process_notification($row, $board_url, $post_data = null) + { + $args = array($row['message']); + + // If forum_id is 0 it's a login notification. + // If forum_id is not 0 it's a post notification, we need to fetch forum name and post subject. + if ($row['forum_id']) + { + $viewtopic_url = append_sid($board_url . 'viewtopic.' . $this->php_ext, array( + 'p' => $row['post_id'], + '#' => 'p' . $row['post_id'], + )); + + $viewforum_url = append_sid($board_url . 'viewforum.' . $this->php_ext, array( + 'f' => $row['forum_id'], + )); + + $args[] = '[url=' . $viewtopic_url . ']' . $post_data['post_subject'] . '[/url]'; + $args[] = '[url=' . $viewforum_url . ']' . $post_data['forum_name'] . '[/url]'; + } + else if ($row['post_id'] == functions::LOGIN_HIDDEN) + { + $row['username'] = '' . $row['username'] . ''; + } + + $row['message'] = call_user_func_array(array($this->user, 'lang'), $args); + + // Quick'n'dirty check if BBCodes are in the message + if (strpos($row['message'], '[') !== false) + { + generate_text_for_storage($row['message'], $row['bbcode_uid'], $row['bbcode_bitfield'], $row['bbcode_options'], true, true, true); + } + + return $row; + } + + /** + * Calculates the number of minutes that have passed since the message was posted. + * If relative time is disabled or the message is older than 59 minutes, -1 is returned. * * @param int $message_age * @return int @@ -852,7 +1280,7 @@ class mchat $this->template->assign_var($option['template_var'], !$is_disallowed); } - $this->template->assign_var('A_MCHAT_DISALLOWED_BBCODES', addslashes(str_replace('=', '-', $this->settings->cfg('mchat_bbcode_disallowed')))); + $this->template->assign_var('A_MCHAT_DISALLOWED_BBCODES', addslashes($this->settings->cfg('mchat_bbcode_disallowed'))); if (!function_exists('display_custom_bbcodes')) { @@ -908,14 +1336,58 @@ class mchat return $sql_ary; } - /** Inserts a message with posting information into the database + /** + * Inserts a message with posting information into the database * - * @param string $mode One of post|quote|edit|reply - * @param $data The post data + * @param string $mode One of post|quote|edit|reply|login + * @param int $forum_id Can be 0 if mode is login. + * @param int $post_id Can be 0 if mode is login. */ - public function insert_posting($mode, $data) + public function insert_posting($mode, $forum_id = 0, $post_id = 0) { - $this->functions->mchat_insert_posting($mode, $data); + $is_hidden_login = $this->request->is_set_post('viewonline') || !$this->user->data['user_allow_viewonline']; + $this->functions->mchat_insert_posting($mode, $forum_id, $post_id, $is_hidden_login); + } + + /** + * Fetches the message text of the given ID, quotes it using the current user name and assigns it to the template + * + * @param int $mchat_message_id + */ + public function quote_message_text($mchat_message_id) + { + if (!$this->auth->acl_get('u_mchat_view')) + { + return; + } + + $rows = $this->functions->mchat_get_messages($mchat_message_id); + $row = reset($rows); + + if (!$row || !$this->has_read_auth($row)) + { + return; + } + + if ($row['post_id']) + { + $rows = array($row); + $this->process_notifications($rows, generate_board_url() . '/'); + $row = reset($rows); + } + + $message_for_edit = generate_text_for_edit($row['message'], $row['bbcode_uid'], $row['bbcode_options']); + $message = '[quote="' . $row['username'] . '"]' . $message_for_edit['text'] . "[/quote]\n"; + + $this->template->assign_var('MESSAGE', $message); + } + + /** + * Remove expired sessions from the database + */ + public function session_gc() + { + $this->functions->mchat_session_gc(); } /** @@ -923,14 +1395,18 @@ class mchat */ protected function assign_whois() { - if ($this->settings->cfg('mchat_whois') || $this->settings->cfg('mchat_stats_index') && $this->settings->cfg('mchat_stats_index')) + if ($this->settings->cfg('mchat_whois_index') || $this->settings->cfg('mchat_stats_index')) { - $mchat_stats = $this->functions->mchat_active_users(); + if ($this->active_users === null) + { + $this->active_users = $this->functions->mchat_active_users(); + } + $this->template->assign_vars(array( - 'MCHAT_STATS_INDEX' => $this->settings->cfg('mchat_stats_index') && $this->settings->cfg('mchat_stats_index'), - 'MCHAT_USERS_COUNT' => $mchat_stats['mchat_users_count'], - 'MCHAT_USERS_LIST' => $mchat_stats['online_userlist'] ?: '', - 'MCHAT_ONLINE_EXPLAIN' => $mchat_stats['refresh_message'], + 'MCHAT_STATS_INDEX' => $this->settings->cfg('mchat_stats_index'), + 'MCHAT_USERS_TOTAL' => $this->active_users['users_total'], + 'MCHAT_USERS_LIST' => $this->active_users['online_userlist'] ?: '', + 'MCHAT_ONLINE_EXPLAIN' => $this->active_users['refresh_message'], )); } } @@ -938,66 +1414,59 @@ class mchat /** * Checks whether an author has edit or delete permissions for a message * - * @param string $permission One of u_mchat_edit|u_mchat_delete + * @param string $mode One of edit|delete * @param int $author_id The user id of the message * @param int $message_time The message created time * @return bool */ - protected function auth_message($permission, $author_id, $message_time) + protected function auth_message($mode, $author_id, $message_time) { - if (!$this->auth->acl_get($permission)) - { - return false; - } - - if ($this->auth->acl_get('m_')) + if ($this->auth->acl_get('u_mchat_moderator_' . $mode)) { return true; } - $can_edit_delete = !$this->settings->cfg('mchat_edit_delete_limit') || $message_time >= time() - $this->settings->cfg('mchat_edit_delete_limit'); - return $can_edit_delete && $this->user->data['user_id'] == $author_id && $this->user->data['is_registered']; + if (!$this->user->data['is_registered'] || $this->user->data['user_id'] != $author_id || !$this->auth->acl_get('u_mchat_' . $mode)) + { + return false; + } + + return !$this->settings->cfg('mchat_edit_delete_limit') || $message_time >= time() - $this->settings->cfg('mchat_edit_delete_limit'); } /** - * Performs bound checks on the message and returns an array containing the message, - * BBCode options and additional data ready to be sent to the database + * Performs bound checks on the message and returns an array containing the message + * and BBCode options ready to be sent to the database * * @param string $message - * @param array $merge_ary * @return array */ - protected function process_message($message, $merge_ary) + protected function process_message($message) { // Must have something other than bbcode in the message - $message_chars = trim(preg_replace('#\[/?[^\[\]]+\]#mi', '', $message)); - if (!utf8_strlen($message_chars)) + $message_without_bbcode = trim(preg_replace('#\[\/?[^\[\]]+\]#m', '', $message)); + if (!utf8_strlen($message_without_bbcode)) { - throw new \phpbb\exception\http_exception(501, 'MCHAT_NOACCESS'); + throw new http_exception(400, 'MCHAT_NOMESSAGEINPUT'); } // Must not exceed character limit if ($this->settings->cfg('mchat_max_message_lngth')) { - if (utf8_strlen($message_chars) > $this->settings->cfg('mchat_max_message_lngth')) + if (utf8_strlen($message) > $this->settings->cfg('mchat_max_message_lngth')) { - throw new \phpbb\exception\http_exception(413, 'MCHAT_MESS_LONG', array($this->settings->cfg('mchat_max_message_lngth'))); + throw new http_exception(400, 'MCHAT_MESS_LONG', array($this->settings->cfg('mchat_max_message_lngth'))); } } - $cfg_min_post_chars = $this->settings->cfg('min_post_chars'); - $cfg_max_post_smilies = $this->settings->cfg('max_post_smilies'); - - // We override the $this->settings->cfg('min_post_chars') entry? if ($this->settings->cfg('mchat_override_min_post_chars')) { - $this->settings->set_cfg('min_post_chars', 0); + $this->settings->set_cfg('min_post_chars', 0, true); } - // We do the same for the max number of smilies? if ($this->settings->cfg('mchat_override_smilie_limit')) { - $this->settings->cfg('max_post_smilies', 0); + $this->settings->set_cfg('max_post_smilies', 0, true); } $mchat_bbcode = $this->settings->cfg('allow_bbcode') && $this->auth->acl_get('u_mchat_bbcode'); @@ -1018,23 +1487,19 @@ class mchat if ($this->settings->cfg('mchat_bbcode_disallowed')) { $bbcode_replace = array( - '#\[(' . $this->settings->cfg('mchat_bbcode_disallowed') . ')[^\[\]]+\]#Usi', - '#\[/(' . $this->settings->cfg('mchat_bbcode_disallowed') . ')[^\[\]]+\]#Usi', + '#\[(' . str_replace('*', '\*', $this->settings->cfg('mchat_bbcode_disallowed')) . ')[^\[\]]+\]#Usi', + '#\[/(' . str_replace('*', '\*', $this->settings->cfg('mchat_bbcode_disallowed')) . ')[^\[\]]+\]#Usi', ); $message = preg_replace($bbcode_replace, '', $message); } - // Reset the config settings - $this->settings->set_cfg('min_post_chars', $cfg_min_post_chars); - $this->settings->set_cfg('max_post_smilies', $cfg_max_post_smilies); - - return array_merge($merge_ary, array( + return array( 'message' => str_replace("'", ''', $message), 'bbcode_bitfield' => $bitfield, 'bbcode_uid' => $uid, 'bbcode_options' => $options, - )); + ); } /** @@ -1043,7 +1508,7 @@ class mchat * @param string $template_file * @return string */ - protected function render_template($template_file) + public function render_template($template_file) { $this->template->set_filenames(array('body' => $template_file)); $content = $this->template->assign_display('body', '', true); diff --git a/core/settings.php b/core/settings.php index bd71852..8dff910 100644 --- a/core/settings.php +++ b/core/settings.php @@ -13,11 +13,6 @@ namespace dmzx\mchat\core; class settings { - const VALIDATE_TYPE = 0; - const VALIDATE_IS_OPTIONAL = 1; - const VALIDATE_MIN_VALUE = 2; - const VALIDATE_MAX_VALUE = 3; - /** @var \phpbb\user */ protected $user; @@ -27,10 +22,23 @@ class settings /** @var \phpbb\auth\auth */ protected $auth; - /** @var array */ + /** + * Keys for global settings that only the administrator is allowed to modify. + * The values are stored in the phpbb_config table. + * + * @var array + */ public $global; - /** @var array */ + /** + * Keys for user-specific settings for which the administrator can set default + * values as well as adjust permissions to allow users to customize them. + * The values are stored in the phpbb_users table as well as the phpbb_config table. + * If a user has permission to customize a setting, the value in the phpbb_users + * table is used, otherwise the value in the phpbb_config table is used. + * + * @var array + */ public $ucp; /** @var bool */ @@ -45,30 +53,62 @@ class settings * @param \phpbb\user $user * @param \phpbb\config\config $config * @param \phpbb\auth\auth $auth - * @param array $global - * @param array $ucp */ - public function __construct(\phpbb\user $user, \phpbb\config\config $config, \phpbb\auth\auth $auth, $global, $ucp) + public function __construct(\phpbb\user $user, \phpbb\config\config $config, \phpbb\auth\auth $auth) { $this->user = $user; $this->config = $config; $this->auth = $auth; - $this->global = $global; - $this->ucp = $ucp; - $this->is_phpbb31 = phpbb_version_compare($config['version'], '3.1.0@dev', '>=') && phpbb_version_compare($config['version'], '3.2.0@dev', '<'); - $this->is_phpbb32 = phpbb_version_compare($config['version'], '3.2.0@dev', '>=') && phpbb_version_compare($config['version'], '3.3.0@dev', '<'); + $this->global = array( + 'mchat_bbcode_disallowed' => array('default' => '', 'validation' => array('string', false, 0, 255)), + 'mchat_custom_height' => array('default' => 350, 'validation' => array('num', false, 50, 1000)), + 'mchat_custom_page' => array('default' => 1), + 'mchat_edit_delete_limit' => array('default' => 0), + 'mchat_flood_time' => array('default' => 0, 'validation' => array('num', false, 0, 60)), + 'mchat_index_height' => array('default' => 250, 'validation' => array('num', false, 50, 1000)), + 'mchat_live_updates' => array('default' => 1), + 'mchat_max_message_lngth' => array('default' => 500, 'validation' => array('num', false, 0, 1000)), + 'mchat_message_num_archive' => array('default' => 25, 'validation' => array('num', false, 10, 100)), + 'mchat_message_num_custom' => array('default' => 10, 'validation' => array('num', false, 5, 50)), + 'mchat_message_num_index' => array('default' => 10, 'validation' => array('num', false, 5, 50)), + 'mchat_navbar_link' => array('default' => 1), + 'mchat_navbar_link_count' => array('default' => 1), + 'mchat_override_min_post_chars' => array('default' => 0), + 'mchat_override_smilie_limit' => array('default' => 0), + 'mchat_posts_edit' => array('default' => 0), + 'mchat_posts_quote' => array('default' => 0), + 'mchat_posts_reply' => array('default' => 0), + 'mchat_posts_topic' => array('default' => 0), + 'mchat_posts_login' => array('default' => 0), + 'mchat_prune' => array('default' => 0), + 'mchat_prune_num' => array('default' => '0'), + 'mchat_refresh' => array('default' => 10, 'validation' => array('num', false, 5, 60)), + 'mchat_rules' => array('default' => '', 'validation' => array('string', false, 0, 255)), + 'mchat_static_message' => array('default' => '', 'validation' => array('string', false, 0, 255)), + 'mchat_timeout' => array('default' => 0, 'validation' => array('num', false, 0, (int) $this->cfg('session_length'))), + 'mchat_whois_refresh' => array('default' => 60, 'validation' => array('num', false, 10, 300)), + ); - $this->inject_core_config_values(); - } + $this->ucp = array( + 'mchat_avatars' => array('default' => 1), + 'mchat_capital_letter' => array('default' => 1), + 'mchat_character_count' => array('default' => 1), + 'mchat_date' => array('default' => 'D M d, Y g:i a', 'validation' => array('string', false, 0, 64)), + 'mchat_index' => array('default' => 1), + 'mchat_input_area' => array('default' => 1), + 'mchat_location' => array('default' => 1), + 'mchat_message_top' => array('default' => 1), + 'mchat_pause_on_input' => array('default' => 0), + 'mchat_posts' => array('default' => 1), + 'mchat_relative_time' => array('default' => 1), + 'mchat_sound' => array('default' => 1), + 'mchat_stats_index' => array('default' => 0), + 'mchat_whois_index' => array('default' => 1), + ); - /** - * Writes phpBB config values into the mChat config for validating input data - */ - protected function inject_core_config_values() - { - // Limit mChat session timeout to phpBB session length - $this->global['mchat_timeout']['validation'][self::VALIDATE_MAX_VALUE] = (int) $this->cfg('session_length'); + $this->is_phpbb31 = phpbb_version_compare(PHPBB_VERSION, '3.1.0@dev', '>=') && phpbb_version_compare(PHPBB_VERSION, '3.2.0@dev', '<'); + $this->is_phpbb32 = phpbb_version_compare(PHPBB_VERSION, '3.2.0@dev', '>=') && phpbb_version_compare(PHPBB_VERSION, '3.3.0@dev', '<'); } /** @@ -101,10 +141,18 @@ class settings /** * @param $config * @param $value + * @param bool $volatile */ - public function set_cfg($config, $value) + public function set_cfg($config, $value, $volatile = false) { - $this->config->set($config, $value); + if ($volatile) + { + $this->config[$config] = $value; + } + else + { + $this->config->set($config, $value); + } } /** @@ -146,7 +194,7 @@ class settings { $enabled_notifications_lang = array(); - foreach (array('topic', 'reply', 'quote', 'edit') as $notification) + foreach (array('topic', 'reply', 'quote', 'edit', 'login') as $notification) { if ($this->cfg('mchat_posts_' . $notification)) { diff --git a/cron/mchat_prune.php b/cron/mchat_prune.php new file mode 100644 index 0000000..2c3f087 --- /dev/null +++ b/cron/mchat_prune.php @@ -0,0 +1,67 @@ +functions = $functions; + $this->settings = $settings; + } + + /** + * Runs this cron task. + * + * @return null + */ + public function run() + { + $this->functions->mchat_prune(); + $this->settings->set_cfg('mchat_prune_last_gc', time()); + } + + /** + * Returns whether this cron task can run, given current board configuration. + * + * If warnings are set to never expire, this cron task will not run. + * + * @return bool + */ + public function is_runnable() + { + return $this->settings->cfg('mchat_prune'); + } + + /** + * Returns whether this cron task should run now, because enough time + * has passed since it was last run (24 hours). + * + * @return bool + */ + public function should_run() + { + return $this->settings->cfg('mchat_prune_last_gc') < time() - $this->settings->cfg('mchat_prune_gc'); + } +} diff --git a/event/acp_listener.php b/event/acp_listener.php index 2c73b4c..b5a655f 100644 --- a/event/acp_listener.php +++ b/event/acp_listener.php @@ -11,6 +11,7 @@ namespace dmzx\mchat\event; +use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class acp_listener implements EventSubscriberInterface @@ -66,11 +67,16 @@ class acp_listener implements EventSubscriberInterface } /** - * @param object $event The event object + * @param Event $event */ public function permissions($event) { - $mchat_permissions = array(); + $ucp_configs = array(); + + foreach (array_keys($this->settings->ucp) as $config_name) + { + $ucp_configs[] = 'u_' . $config_name; + } $permission_categories = array( 'mchat' => array( @@ -78,6 +84,8 @@ class acp_listener implements EventSubscriberInterface 'u_mchat_view', 'u_mchat_edit', 'u_mchat_delete', + 'u_mchat_moderator_edit', + 'u_mchat_moderator_delete', 'u_mchat_ip', 'u_mchat_pm', 'u_mchat_like', @@ -89,9 +97,11 @@ class acp_listener implements EventSubscriberInterface 'u_mchat_urls', 'a_mchat', ), - 'mchat_user_config' => array_map(function($key) { return 'u_' . $key; }, array_keys($this->settings->ucp)), + 'mchat_user_config' => $ucp_configs, ); + $mchat_permissions = array(); + foreach ($permission_categories as $cat => $permissions) { foreach ($permissions as $permission) @@ -107,12 +117,12 @@ class acp_listener implements EventSubscriberInterface $event['categories'] = array_merge($event['categories'], array( 'mchat' => 'ACP_CAT_MCHAT', - 'mchat_user_config' => 'ACP_CAT_MCHAT_USER_CONFIG' + 'mchat_user_config' => 'ACP_CAT_MCHAT_USER_CONFIG', )); } /** - * @param object $event The event object + * @param Event $event */ public function acp_users_prefs_modify_sql($event) { @@ -150,7 +160,7 @@ class acp_listener implements EventSubscriberInterface } /** - * @param object $event The event object + * @param Event $event */ public function acp_users_prefs_modify_template_data($event) { diff --git a/event/main_listener.php b/event/main_listener.php index 2b25236..000a120 100644 --- a/event/main_listener.php +++ b/event/main_listener.php @@ -11,6 +11,7 @@ namespace dmzx\mchat\event; +use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class main_listener implements EventSubscriberInterface @@ -24,6 +25,9 @@ class main_listener implements EventSubscriberInterface /** @var \phpbb\user */ protected $user; + /** @var \phpbb\request\request */ + protected $request; + /** @var string */ protected $php_ext; @@ -33,14 +37,16 @@ class main_listener implements EventSubscriberInterface * @param \dmzx\mchat\core\mchat $mchat * @param \phpbb\controller\helper $helper * @param \phpbb\user $user + * @param \phpbb\request\request $request * @param string $php_ext */ - public function __construct(\dmzx\mchat\core\mchat $mchat, \phpbb\controller\helper $helper, \phpbb\user $user, $php_ext) + public function __construct(\dmzx\mchat\core\mchat $mchat, \phpbb\controller\helper $helper, \phpbb\user $user, \phpbb\request\request $request, $php_ext) { - $this->mchat = $mchat; - $this->helper = $helper; - $this->user = $user; - $this->php_ext = $php_ext; + $this->mchat = $mchat; + $this->helper = $helper; + $this->user = $user; + $this->request = $request; + $this->php_ext = $php_ext; } /** @@ -53,26 +59,28 @@ class main_listener implements EventSubscriberInterface 'core.user_setup' => 'load_language_on_setup', 'core.page_header' => 'add_page_header_link', 'core.index_modify_page_title' => 'display_mchat_on_index', - 'core.posting_modify_submit_post_after' => 'posting_modify_submit_post_after', - 'core.display_custom_bbcodes_modify_sql' => 'display_custom_bbcodes_modify_sql', + 'core.submit_post_end' => 'insert_posting', + 'core.display_custom_bbcodes_modify_sql' => array(array('remove_disallowed_bbcodes'), array('pm_compose_add_quote')), 'core.user_add_modify_data' => 'user_registration_set_default_values', + 'core.login_box_redirect' => 'user_login_success', + 'core.session_gc_after' => 'session_gc', ); } /** - * @param object $event The event object + * @param Event $event */ public function add_page_viewonline($event) { if (strrpos($event['row']['session_page'], 'app.' . $this->php_ext . '/mchat') === 0) { $event['location'] = $this->user->lang('MCHAT_TITLE'); - $event['location_url'] = $this->helper->route('dmzx_mchat_controller'); + $event['location_url'] = $this->helper->route('dmzx_mchat_page_custom_controller'); } } /** - * @param object $event The event object + * @param Event $event */ public function load_language_on_setup($event) { @@ -87,7 +95,7 @@ class main_listener implements EventSubscriberInterface /** * Create a URL to the mchat controller file for the header linklist * - * @param object $event The event object + * @param Event $event */ public function add_page_header_link($event) { @@ -97,7 +105,7 @@ class main_listener implements EventSubscriberInterface /** * Check if mchat should be displayed on index. * - * @param object $event The event object + * @param Event $event */ public function display_mchat_on_index($event) { @@ -105,31 +113,58 @@ class main_listener implements EventSubscriberInterface } /** - * @param object $event The event object + * @param Event $event */ - public function posting_modify_submit_post_after($event) + public function insert_posting($event) { - $this->mchat->insert_posting($event['mode'], array( - 'forum_id' => $event['forum_id'], - 'forum_name' => $event['post_data']['forum_name'], - 'post_id' => $event['data']['post_id'], - 'post_subject' => $event['post_data']['post_subject'], - )); + $this->mchat->insert_posting($event['mode'], $event['data']['forum_id'], $event['data']['post_id']); } /** - * @param object $event The event object + * @param Event $event */ - public function display_custom_bbcodes_modify_sql($event) + public function remove_disallowed_bbcodes($event) { $event['sql_ary'] = $this->mchat->remove_disallowed_bbcodes($event['sql_ary']); } /** - * @param object $event The event object + * @param Event $event */ public function user_registration_set_default_values($event) { $event['sql_ary'] = $this->mchat->set_user_default_values($event['sql_ary']); } + + /** + * @param Event $event + */ + public function user_login_success($event) + { + if (!$event['admin']) + { + $this->mchat->insert_posting('login'); + } + } + + /** + * @param Event $event + */ + public function pm_compose_add_quote($event) + { + $mchat_message_id = $this->request->variable('mchat_pm_quote_message', 0); + + if ($mchat_message_id) + { + $this->mchat->quote_message_text($mchat_message_id); + } + } + + /** + * @param Event $event + */ + public function session_gc($event) + { + $this->mchat->session_gc(); + } } diff --git a/ext.php b/ext.php index 5ebaaaa..6fed5ca 100644 --- a/ext.php +++ b/ext.php @@ -22,6 +22,77 @@ class ext extends \phpbb\extension\base public function is_enableable() { $config = $this->container->get('config'); + + // Here we check if any modules from the mChat MOD for phpBB 3.0.x are still in the database. + // This is_enableable() method is called multiple times during the installation but we only + // need to do the following check once. Checking for the absence of the mchat_version value + // in the config guarantees that we're in the very first step of the installation process. + // Any later call of this method doesn't need to check this again and in fact will wrongly + // detect the extension's modules as being remnants. + if (empty($config['mchat_version'])) + { + $table_prefix = $this->container->getParameter('core.table_prefix'); + $module_ids = $this->get_old_module_ids($table_prefix); + + if ($module_ids) + { + if (phpbb_version_compare($config['version'], '3.2.0-dev', '>=')) + { + // For phpBB 3.2.x + $lang = $this->container->get('language'); + $lang->add_lang('mchat_acp', 'dmzx/mchat'); + } + else + { + // For phpBB 3.1.x + $user = $this->container->get('user'); + $user->add_lang_ext('dmzx/mchat', 'mchat_acp'); + $lang = $user; + } + + $php_ext = $this->container->getParameter('core.php_ext'); + $error_msg = $lang->lang('MCHAT_30X_REMNANTS', $table_prefix, implode(', ', $module_ids)) . adm_back_link(append_sid('index.' . $php_ext, 'i=acp_extensions&mode=main')); + + trigger_error($error_msg, E_USER_WARNING); + } + } + return phpbb_version_compare($config['version'], '3.1.7-PL1', '>='); } + + /** + * This method checks whether the phpbb_modules table contains remnants of the 3.0 MOD. + * It returns an array of the modules' IDs, or an empty array if no old modules are found. + * + * @var string $table_prefix + * @return array + */ + protected function get_old_module_ids($table_prefix) + { + $db = $this->container->get('dbal.conn'); + + $mchat_30x_module_langnames = array( + 'ACP_CAT_MCHAT', + 'ACP_MCHAT_CONFIG', + 'ACP_USER_MCHAT', + 'UCP_CAT_MCHAT', + 'UCP_MCHAT_CONFIG', + ); + + $sql = 'SELECT module_id + FROM ' . $table_prefix . 'modules + WHERE ' . $db->sql_in_set('module_langname', $mchat_30x_module_langnames); + $result = $db->sql_query($sql); + $rows = $db->sql_fetchrowset(); + $db->sql_freeresult($result); + + $module_ids = array(); + + foreach ($rows as $row) + { + $module_ids[] = $row['module_id']; + } + + return $module_ids; + } } diff --git a/language/en/common.php b/language/en/common.php index f4295e2..cbbd80f 100644 --- a/language/en/common.php +++ b/language/en/common.php @@ -36,13 +36,14 @@ if (empty($lang) || !is_array($lang)) $lang = array_merge($lang, array( 'MCHAT_TITLE' => 'mChat', + 'MCHAT_TITLE_COUNT' => 'mChat [%1$d]', // Who is chatting 'MCHAT_WHO_IS_CHATTING' => 'Who is chatting', 'MCHAT_ONLINE_USERS_TOTAL' => array( 0 => 'No one is chatting', - 1 => 'There is %1$d user chatting', - 2 => 'There are %1$d users chatting', + 1 => '%1$d user is chatting', + 2 => '%1$d users are chatting', ), 'MCHAT_ONLINE_EXPLAIN' => 'based on users active over the past %1$s', 'MCHAT_HOURS' => array( @@ -63,4 +64,5 @@ $lang = array_merge($lang, array( 'MCHAT_NEW_REPLY' => 'posted a reply: %1$s in %2$s', 'MCHAT_NEW_QUOTE' => 'replied with a quote: %1$s in %2$s', 'MCHAT_NEW_EDIT' => 'edited a post: %1$s in %2$s', + 'MCHAT_NEW_LOGIN' => 'just logged in', )); diff --git a/language/en/info_acp_mchat.php b/language/en/info_acp_mchat.php index 1b42daa..e05eca2 100644 --- a/language/en/info_acp_mchat.php +++ b/language/en/info_acp_mchat.php @@ -43,7 +43,8 @@ $lang = array_merge($lang, array( // Log entries (%1$s is replaced with the user name who triggered the event) 'LOG_MCHAT_CONFIG_UPDATE' => 'mChat configuration updated
» %1$s', - 'LOG_MCHAT_TABLE_PRUNED' => 'mChat messages pruned
» %1$s', + 'LOG_MCHAT_TABLE_PRUNED' => 'mChat messages pruned: %2$d
» %1$s', + 'LOG_MCHAT_TABLE_PRUNE_FAIL' => 'mChat pruning failed: invalid time period
» %1$s', 'LOG_MCHAT_TABLE_PURGED' => 'mChat messages purged
» %1$s', 'LOG_DELETED_MCHAT' => 'mChat message deleted
» %1$s', 'LOG_EDITED_MCHAT' => 'mChat message edited
» %1$s', diff --git a/language/en/mchat.php b/language/en/mchat.php index c7d547c..d73561f 100644 --- a/language/en/mchat.php +++ b/language/en/mchat.php @@ -40,23 +40,22 @@ $lang = array_merge($lang, array( 'MCHAT_ARCHIVE_PAGE' => 'mChat Archive', 'MCHAT_BBCODES' => 'BBCodes', 'MCHAT_CUSTOM_BBCODES' => 'Custom BBCodes', - 'MCHAT_DELCONFIRM' => 'Do you confirm removal?', + 'MCHAT_DELCONFIRM' => 'Are you sure you want to delete this message?', 'MCHAT_EDIT' => 'Edit', - 'MCHAT_EDITINFO' => 'Edit the message and click OK', + 'MCHAT_EDITINFO' => 'Edit the message below.', 'MCHAT_NEW_CHAT' => 'New chat message!', 'MCHAT_SEND_PM' => 'Send private message', - 'MCHAT_LIKE' => 'Like this post', - 'MCHAT_LIKES' => 'likes this post', - 'MCHAT_FLOOD' => 'You can not post another message so soon after your last', + 'MCHAT_LIKE' => 'Like this message', + 'MCHAT_LIKES' => 'likes this message', + 'MCHAT_FLOOD' => 'You can not post another message so soon after your last.', 'MCHAT_FOE' => 'This message was made by %1$s who is currently on your ignore list.', 'MCHAT_RULES' => 'Rules', 'MCHAT_WHOIS_USER' => 'IP whois for %1$s', - 'MCHAT_MESS_LONG' => 'Your message is too long. Please limit it to %1$d characters', - 'MCHAT_NO_CUSTOM_PAGE' => 'The mChat custom page is not activated at this time!', - 'MCHAT_NO_RULES' => 'The mChat rules page is not activated at this time!', - 'MCHAT_NOACCESS' => 'You don’t have permission to post in the chat', - 'MCHAT_NOACCESS_ARCHIVE' => 'You don’t have permission to view the archive', - 'MCHAT_NOJAVASCRIPT' => 'Your browser does not support JavaScript or JavaScript is disabled', + 'MCHAT_MESS_LONG' => 'Your message is too long. Please limit it to %1$d characters.', + 'MCHAT_NO_CUSTOM_PAGE' => 'The mChat custom page is not activated at this time.', + 'MCHAT_NO_RULES' => 'The mChat rules page is not activated at this time.', + 'MCHAT_NOACCESS_ARCHIVE' => 'You don’t have permission to view the archive.', + 'MCHAT_NOJAVASCRIPT' => 'Please enable JavaScript to use mChat.', 'MCHAT_NOMESSAGE' => 'No messages', 'MCHAT_NOMESSAGEINPUT' => 'You have not entered a message', 'MCHAT_OK' => 'OK', @@ -79,7 +78,7 @@ $lang = array_merge($lang, array( 2 => '%1$d minutes ago', ), - // These messages are formatted with JavaScript, hence {} and no $d + // These messages are formatted with JavaScript, hence {} and no %d 'MCHAT_CHARACTER_COUNT' => '{current} characters', 'MCHAT_CHARACTER_COUNT_LIMIT' => '{current} out of {max} characters', 'MCHAT_SESSION_ENDS_JS' => 'Chat session ends in {timeleft}', diff --git a/language/en/mchat_acp.php b/language/en/mchat_acp.php index cc8c9a0..8be7605 100644 --- a/language/en/mchat_acp.php +++ b/language/en/mchat_acp.php @@ -41,7 +41,7 @@ $lang = array_merge($lang, array( 'MCHAT_SETTINGS_ARCHIVE' => 'Archive page settings', 'MCHAT_SETTINGS_POSTS' => 'New posts settings', 'MCHAT_SETTINGS_MESSAGES' => 'Message settings', - 'MCHAT_SETTINGS_PRUNE' => 'Pruning settings', + 'MCHAT_SETTINGS_PRUNE' => 'Pruning settings (adjustable for founders only)', 'MCHAT_SETTINGS_STATS' => 'Who is chatting settings', 'MCHAT_GLOBALUSERSETTINGS_EXPLAIN' => 'Settings for which a user does not have permission to customise are applied as configured below.
New user accounts will have initial settings as configured below.

Go to the mChat in UCP tab of the user permissions section to adjust customisation permissions.
Go to the Preferences form in the user management section to see the status of each user’s settings.', @@ -70,9 +70,14 @@ $lang = array_merge($lang, array( 'MCHAT_LIVE_UPDATES' => 'Live updates of edited and deleted messages', 'MCHAT_LIVE_UPDATES_EXPLAIN' => 'When a user edits or deletes messages, the changes are updated live for all others, without them having to refresh the page. Disable this if you experience performance issues.', 'MCHAT_PRUNE' => 'Enable message pruning', - 'MCHAT_PRUNE_EXPLAIN' => 'Only occurs if a user views the custom or archive pages.', - 'MCHAT_PRUNE_NUM' => 'Number of messages to retain when pruning', + 'MCHAT_PRUNE_EXPLAIN' => 'The messages table is pruned every 24 hours.', + 'MCHAT_PRUNE_NUM' => 'Messages to retain when pruning', + 'MCHAT_PRUNE_NUM_EXPLAIN' => 'You can specify either a number to keep a fixed number of messages (example: 42) or a time period (examples: 24 hours, 5 days, 2 weeks, 1 month). All messages older than the time period at the time of pruning will be deleted.', + 'MCHAT_PRUNE_NOW' => 'Prune messages now', + 'MCHAT_PRUNE_NOW_CONFIRM' => 'Confirm pruning messages', + 'MCHAT_PRUNED' => '%1$d mChat messages have been pruned', 'MCHAT_NAVBAR_LINK' => 'Display link to the custom page in the navbar', + 'MCHAT_NAVBAR_LINK_COUNT' => 'Display number of active chat sessions in navbar link', 'MCHAT_MESSAGE_NUM_CUSTOM' => 'Initial number of messages to display on the custom page', 'MCHAT_MESSAGE_NUM_CUSTOM_EXPLAIN' => 'You are limited from 5 to 50. Default is 10.', 'MCHAT_MESSAGE_NUM_INDEX' => 'Initial number of messages to display on the index page', @@ -93,8 +98,8 @@ $lang = array_merge($lang, array( 'MCHAT_BBCODES_DISALLOWED_EXPLAIN' => 'Here you can input the bbcodes that are not to be used in a message.
Separate bbcodes with a vertical bar, for example:
b|i|u|code|list|list=|flash|quote and/or a %1$scustom bbcode tag name%2$s', 'MCHAT_STATIC_MESSAGE' => 'Static message', 'MCHAT_STATIC_MESSAGE_EXPLAIN' => 'Here you can define a static message to display to users of the chat. HTML code is allowed.
Set to empty to disable the display. You are limited to 255 characters.
This message can be translated: edit the MCHAT_STATIC_MESSAGE language key in /ext/dmzx/mchat/language/XX/mchat.php.', - 'MCHAT_USER_TIMEOUT' => 'User session timeout', - 'MCHAT_USER_TIMEOUT_EXPLAIN' => 'Set the amount of time in seconds until a user session in the chat ends.
Set to 0 for no timeout. Careful, the session of a user reading mChat will never expire!
You are limited to the %1$sforum config setting for sessions%2$s which is currently set to %3$d seconds', + 'MCHAT_TIMEOUT' => 'Session timeout', + 'MCHAT_TIMEOUT_EXPLAIN' => 'Set the number of seconds until a session in the chat ends.
Set to 0 for no timeout. Careful, the session of a user reading mChat will never expire!
You are limited to the %1$sforum config setting for sessions%2$s which is currently set to %3$d seconds', 'MCHAT_OVERRIDE_SMILIE_LIMIT' => 'Override smilie limit', 'MCHAT_OVERRIDE_SMILIE_LIMIT_EXPLAIN' => 'Set to yes to override the forums smilie limit setting for chat messages', 'MCHAT_OVERRIDE_MIN_POST_CHARS' => 'Override minimum characters limit', @@ -132,4 +137,6 @@ $lang = array_merge($lang, array( 'TOO_LARGE_MCHAT_TIMEOUT' => 'The user timeout value is too large.', 'TOO_SMALL_MCHAT_WHOIS_REFRESH' => 'The whois refresh value is too small.', 'TOO_LARGE_MCHAT_WHOIS_REFRESH' => 'The whois refresh value is too large.', + + 'MCHAT_30X_REMNANTS' => 'The installation as been aborted.
There are remnant modules from the mChat MOD for phpBB 3.0.x in the database. The mChat extension does not work correctly with these modules present.
You need to entirely uninstall the mChat MOD before being able to install the mChat extension. Specifically, the modules with the following IDs need to be deleted from the %1$smodules table: %2$s', )); diff --git a/language/en/mchat_ucp.php b/language/en/mchat_ucp.php index b1fdea1..446268b 100644 --- a/language/en/mchat_ucp.php +++ b/language/en/mchat_ucp.php @@ -51,7 +51,7 @@ $lang = array_merge($lang, array( 'MCHAT_POSTS' => 'Display new posts (currently all disabled, can be enabled in the mChat Global Settings section in the ACP)', 'MCHAT_CHARACTER_COUNT' => 'Display number of characters when typing a message', 'MCHAT_RELATIVE_TIME' => 'Display relative time for new messages', - 'MCHAT_RELATIVE_TIME_EXPLAIN' => 'Displays "just now", "1 minute ago" and so on for each message. Set to No to always display the full date.', + 'MCHAT_RELATIVE_TIME_EXPLAIN' => 'Displays “just now”, “1 minute ago” and so on for each message. Set to No to always display the full date.', 'MCHAT_PAUSE_ON_INPUT' => 'Pause on input', 'MCHAT_PAUSE_ON_INPUT_EXPLAIN' => 'Do not update mChat upon entering a message', 'MCHAT_MESSAGE_TOP' => 'Location of new chat messages', @@ -64,6 +64,7 @@ $lang = array_merge($lang, array( 'MCHAT_POSTS_REPLY' => 'Display new replies', 'MCHAT_POSTS_EDIT' => 'Display edited posts', 'MCHAT_POSTS_QUOTE' => 'Display quoted posts', + 'MCHAT_POSTS_LOGIN' => 'Display user logins', 'MCHAT_DATE_FORMAT' => 'Date format', 'MCHAT_DATE_FORMAT_EXPLAIN' => 'The syntax used is identical to the PHP date() function.', diff --git a/language/en/permissions_mchat.php b/language/en/permissions_mchat.php index 4e3751c..b646078 100644 --- a/language/en/permissions_mchat.php +++ b/language/en/permissions_mchat.php @@ -37,8 +37,10 @@ if (empty($lang) || !is_array($lang)) $lang = array_merge($lang, array( 'ACL_U_MCHAT_USE' => 'Can use mChat', 'ACL_U_MCHAT_VIEW' => 'Can view mChat', - 'ACL_U_MCHAT_EDIT' => 'Can edit messages', - 'ACL_U_MCHAT_DELETE' => 'Can delete messages', + 'ACL_U_MCHAT_EDIT' => 'Can edit own messages', + 'ACL_U_MCHAT_DELETE' => 'Can delete own messages', + 'ACL_U_MCHAT_MODERATOR_EDIT' => 'Can edit anyone’s messages', + 'ACL_U_MCHAT_MODERATOR_DELETE' => 'Can delete anyone’s messages', 'ACL_U_MCHAT_IP' => 'Can view IP addresses', 'ACL_U_MCHAT_PM' => 'Can use private message', 'ACL_U_MCHAT_LIKE' => 'Can like messages', @@ -47,7 +49,7 @@ $lang = array_merge($lang, array( 'ACL_U_MCHAT_ARCHIVE' => 'Can view the archive', 'ACL_U_MCHAT_BBCODE' => 'Can use BBCodes', 'ACL_U_MCHAT_SMILIES' => 'Can use smilies', - 'ACL_U_MCHAT_URLS' => 'Can post URLs', + 'ACL_U_MCHAT_URLS' => 'Can post automatically parsed URLs', 'ACL_U_MCHAT_AVATARS' => 'Can customise Display avatars', 'ACL_U_MCHAT_CAPITAL_LETTER' => 'Can customise Capital first letter', diff --git a/migrations/mchat_2_0_0_rc3.php b/migrations/mchat_2_0_0_rc3.php index edf8ff6..0eb085d 100644 --- a/migrations/mchat_2_0_0_rc3.php +++ b/migrations/mchat_2_0_0_rc3.php @@ -13,49 +13,60 @@ namespace dmzx\mchat\migrations; class mchat_2_0_0_rc3 extends \phpbb\db\migration\migration { - /** @var array */ - protected $mchat_config = null; - static public function depends_on() { return array('\phpbb\db\migration\data\v31x\v317pl1'); } - protected function get_config() - { - if ($this->mchat_config == null) - { - $yml_config_file = $this->phpbb_root_path . '/ext/dmzx/mchat/config/config_2_0_0.yml'; - $yml_data = \Symfony\Component\Yaml\Yaml::parse($yml_config_file); - $this->mchat_config = $yml_data['parameters']; - } - - return $this->mchat_config; - } - public function update_data() { - $config = $this->get_config(); - $update_data = array(); - - // Add configs - foreach (array('dmzx.mchat.config_global', 'dmzx.mchat.config_ucp') as $section) - { - foreach ($config[$section] as $key => $value) - { - $update_data[] = array('config.add', array($key, $value['default'])); - } - } - - // Add user permissions for customizing config values - foreach ($config['dmzx.mchat.config_ucp'] as $key => $value) - { - $update_data[] = array('permission.add', array('u_' . $key, true)); - } - - return array_merge($update_data, array( + return array( array('config.add', array('mchat_version', '2.0.0-RC3')), + // Add global configs + array('config.add', array('mchat_bbcode_disallowed', '')), + array('config.add', array('mchat_custom_height', 350)), + array('config.add', array('mchat_custom_page', 1)), + array('config.add', array('mchat_edit_delete_limit', 0)), + array('config.add', array('mchat_flood_time', 0)), + array('config.add', array('mchat_index_height', 250)), + array('config.add', array('mchat_live_updates', 1)), + array('config.add', array('mchat_max_message_lngth', 500)), + array('config.add', array('mchat_message_num_archive', 25)), + array('config.add', array('mchat_message_num_custom', 10)), + array('config.add', array('mchat_message_num_index', 10)), + array('config.add', array('mchat_navbar_link', 1)), + array('config.add', array('mchat_override_min_post_chars', 0)), + array('config.add', array('mchat_override_smilie_limit', 0)), + array('config.add', array('mchat_posts_edit', 0)), + array('config.add', array('mchat_posts_quote', 0)), + array('config.add', array('mchat_posts_reply', 0)), + array('config.add', array('mchat_posts_topic', 0)), + array('config.add', array('mchat_prune', 0)), + array('config.add', array('mchat_prune_num', '0')), + array('config.add', array('mchat_refresh', 10)), + array('config.add', array('mchat_rules', '')), + array('config.add', array('mchat_static_message', '')), + array('config.add', array('mchat_timeout', 0)), + array('config.add', array('mchat_whois', 1)), + array('config.add', array('mchat_whois_refresh', 60)), + + // Add global user configs + array('config.add', array('mchat_avatars', 1)), + array('config.add', array('mchat_capital_letter', 1)), + array('config.add', array('mchat_character_count', 1)), + array('config.add', array('mchat_date', 'D M d, Y g:i a')), + array('config.add', array('mchat_index', 1)), + array('config.add', array('mchat_input_area', 1)), + array('config.add', array('mchat_location', 1)), + array('config.add', array('mchat_message_top', 1)), + array('config.add', array('mchat_pause_on_input', 0)), + array('config.add', array('mchat_posts', 1)), + array('config.add', array('mchat_relative_time', 1)), + array('config.add', array('mchat_sound', 1)), + array('config.add', array('mchat_stats_index', 0)), + array('config.add', array('mchat_whois_index', 1)), + // Add user permissions array('permission.add', array('u_mchat_use', true)), array('permission.add', array('u_mchat_view', true)), @@ -71,6 +82,22 @@ class mchat_2_0_0_rc3 extends \phpbb\db\migration\migration array('permission.add', array('u_mchat_smilies', true)), array('permission.add', array('u_mchat_urls', true)), + // Add user permissions for customizing config values + array('permission.add', array('u_mchat_avatars', true)), + array('permission.add', array('u_mchat_capital_letter', true)), + array('permission.add', array('u_mchat_character_count', true)), + array('permission.add', array('u_mchat_date', true)), + array('permission.add', array('u_mchat_index', true)), + array('permission.add', array('u_mchat_input_area', true)), + array('permission.add', array('u_mchat_location', true)), + array('permission.add', array('u_mchat_message_top', true)), + array('permission.add', array('u_mchat_pause_on_input', true)), + array('permission.add', array('u_mchat_posts', true)), + array('permission.add', array('u_mchat_relative_time', true)), + array('permission.add', array('u_mchat_sound', true)), + array('permission.add', array('u_mchat_stats_index', true)), + array('permission.add', array('u_mchat_whois_index', true)), + // Add admin permissions array('permission.add', array('a_mchat', true)), @@ -129,22 +156,14 @@ class mchat_2_0_0_rc3 extends \phpbb\db\migration\migration 'UCP_MCHAT_CONFIG', array('module_basename' => '\dmzx\mchat\ucp\ucp_mchat_module'), )), - )); + ); } public function update_schema() { - $config = $this->get_config(); - $user_columns = array(); - - foreach ($config['dmzx.mchat.config_ucp'] as $key => $value) - { - $user_columns['user_' . $key] = array($value['type'], $value['default']); - } - return array( - 'add_tables' => array( - $this->table_prefix . 'mchat' => array( + 'add_tables' => array( + $this->table_prefix . 'mchat' => array( 'COLUMNS' => array( 'message_id' => array('UINT', null, 'auto_increment'), 'user_id' => array('UINT', 0), @@ -163,7 +182,7 @@ class mchat_2_0_0_rc3 extends \phpbb\db\migration\migration $this->table_prefix . 'mchat_deleted_messages' => array( 'COLUMNS' => array( - 'message_id' => array('UINT', null), + 'message_id' => array('UINT', 0), ), 'PRIMARY_KEY' => 'message_id', ), @@ -178,31 +197,53 @@ class mchat_2_0_0_rc3 extends \phpbb\db\migration\migration ), ), - 'add_columns' => array( - $this->table_prefix . 'users' => $user_columns, + 'add_columns' => array( + $this->table_prefix . 'users' => array( + 'user_mchat_avatars' => array('BOOL', 1), + 'user_mchat_capital_letter' => array('BOOL', 1), + 'user_mchat_character_count' => array('BOOL', 1), + 'user_mchat_date' => array('VCHAR:64', 'D M d, Y g:i a'), + 'user_mchat_index' => array('BOOL', 1), + 'user_mchat_input_area' => array('BOOL', 1), + 'user_mchat_location' => array('BOOL', 1), + 'user_mchat_message_top' => array('BOOL', 1), + 'user_mchat_pause_on_input' => array('BOOL', 0), + 'user_mchat_posts' => array('BOOL', 1), + 'user_mchat_relative_time' => array('BOOL', 1), + 'user_mchat_sound' => array('BOOL', 1), + 'user_mchat_stats_index' => array('BOOL', 0), + 'user_mchat_whois_index' => array('BOOL', 1), + ), ), ); } public function revert_schema() { - $config = $this->get_config(); - $user_columns = array(); - - foreach (array_keys($config['dmzx.mchat.config_ucp']) as $key) - { - $user_columns[] = 'user_' . $key; - } - return array( - 'drop_tables' => array( + 'drop_tables' => array( $this->table_prefix . 'mchat', $this->table_prefix . 'mchat_deleted_messages', $this->table_prefix . 'mchat_sessions', ), - 'drop_columns' => array( - $this->table_prefix . 'users' => $user_columns, + 'drop_columns' => array( + $this->table_prefix . 'users' => array( + 'user_mchat_avatars', + 'user_mchat_capital_letter', + 'user_mchat_character_count', + 'user_mchat_date', + 'user_mchat_index', + 'user_mchat_input_area', + 'user_mchat_location', + 'user_mchat_message_top', + 'user_mchat_pause_on_input', + 'user_mchat_posts', + 'user_mchat_relative_time', + 'user_mchat_sound', + 'user_mchat_stats_index', + 'user_mchat_whois_index', + ), ), ); } diff --git a/migrations/mchat_2_0_0_rc6.php b/migrations/mchat_2_0_0_rc6.php new file mode 100644 index 0000000..7448621 --- /dev/null +++ b/migrations/mchat_2_0_0_rc6.php @@ -0,0 +1,77 @@ + value is dynamic, do not cache + + array('config.remove', array('mchat_whois')), + + array('permission.add', array('u_mchat_moderator_edit', true)), + array('permission.add', array('u_mchat_moderator_delete', true)), + ); + } + + public function update_schema() + { + return array( + 'add_tables' => array( + $this->table_prefix . 'mchat_log' => array( + 'COLUMNS' => array( + 'log_id' => array('UINT', null, 'auto_increment'), + 'log_type' => array('TINT:4', 0), + 'user_id' => array('UINT', 0), + 'message_id' => array('UINT', 0), + 'log_ip' => array('VCHAR:40', ''), + 'log_time' => array('INT:11', 0), + ), + 'PRIMARY_KEY' => 'log_id', + ), + ), + 'drop_tables' => array( + $this->table_prefix . 'mchat_deleted_messages', + ), + + 'drop_columns' => array( + $this->table_prefix . 'mchat' => array( + 'edit_time', + ), + ), + ); + } + + public function revert_schema() + { + return array( + 'drop_tables' => array( + $this->table_prefix . 'mchat_log', + ), + ); + } +} diff --git a/sounds/README.txt b/sounds/README.txt new file mode 100644 index 0000000..2377467 --- /dev/null +++ b/sounds/README.txt @@ -0,0 +1,6 @@ +Credits + +add.mp3 UI Buttons and Whooshes Pack 1 by Narfstuff http://www.narfstuff.co.uk/ +edit.mp3 Scribble by TiesWijnen https://www.freesound.org/people/TiesWijnen/sounds/341738/ +del.mp3 Paper Throw http://www.soundjay.com/ +error.mp3 GUI Sound Effects by Lokif http://opengameart.org/content/gui-sound-effects diff --git a/sounds/add.mp3 b/sounds/add.mp3 index 2df0d2e09556fe478186e1e98ae57c43fa6a98aa..58556a3ac8e60df5d260f77768bed9d4873c8823 100644 GIT binary patch literal 6146 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 literal 13210 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 RTm}D+RsH`>@&1po{2w`l532wG literal 7254 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 diff --git a/sounds/edit.mp3 b/sounds/edit.mp3 index a87118760586e3398033307fd8b604ec8bab7e17..96672efd360cfa376b63bf0ac88707d91cef10bc 100644 GIT binary patch literal 4899 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 QARGWN{zq^0|Jz0X1ziV)RR910 diff --git a/sounds/error.mp3 b/sounds/error.mp3 index af9a34ccce80cab546bd2174f807a85f39cfc47e..829dffbbd39827851bb53233a6320bfbb7f7e585 100644 GIT binary patch literal 3228 zcmds(YgCg*8i40ZLI@ZjU~oa8CgF}6lMsYZz(B%1Aot~J0|Ig@HxU=47(xhOsRj`Q z1%+~v0jM~!yTAAc~Xv+TPjTzJBnXy!p~NU#S3ae2b|t)^J^go-0QLqf{T11 zpN32fzyZ|86bzwh97Jwe&Z@Ugyk0AQdZAx;s~VZ`(Z@9q;bA4urpVJaV`ziRw0GhY zL!Z+w+1Bx(A;l^WE6q@o(!_8XgI8;6AaEgL zkbY(WP(tzPUir?4Q5ND2w_t0|TY>)ID;=e1RnY=K99L5C?%q$F+Fxw}(-FO&l-iZ{ zHg&bIIVKKuu!$Cr3Md?dtYA_$K!i)`q2y(C_B)hoc8v!mY3+8daz&lHl7#Ojn+ER9 z+sfzMDy@Z{qavF+LQs)Y;TD@lir{dR+mWsm(}Qzl{g;LseiZCGqpSdG!!#)2>t`Z0 za$cpX!=#~3I>}`Bp@nzwIwS$bzh3{qdubeR(=Rp-s3QiA#~$};SyI3E=q?kvH_N6p zP-a@5)mlb**)DZ|TZjx`NotN#rzU|$YcjZrg3xx&e)p<4DngWA9%Th)5WP9RJlJTr zVub?C;JNWH?~F$wCFhCsSilKzN%1TJ9^+mb*3!?Y>YRwxQ&g21k@`8vGSVC4qWA6L z0}O$qLVSFGiAHZ2TT}O`fg(3IHWuUE+gI6+?HwtOp3|%9oBt#de&`o>tz}B`LthI6 zq_XIf)B(>qg&PMBt*v}bX-J5R^m$bI&DsMdf8pIXbfb}1e-~d7(y7Aja02=5l6c^% z8Ngs$NCrTw%45-N5dx3T9_Uj6$hxDZs%;!pTE-3dVK#)!zqpu z-R_MeS|*e7c>c=ELea%DS5>@B!m9M^<>nOi0CMgDRt1aIf*6SDghy$82Bzn52>@y% z4q}V15C#IDdozMhcZU)@G|dpDnLfqg0i0^Ma){FzH`Rpbqb{lYnKi)USBcpuR%2TT z!GZkIzB++81U^k7ga$};pJPQC91M?t;3tv*wrBJa-Tj){O8hXI*G%T~p782BcinCm z8M4=4{5t#tEV0_KzRGe*#P-H;r~kr&=e?f7)h@tSZ`N)uaXZtcNdP~7s@bJ6&tHQy zMV59xl>CX^%EmB5M;9Aj+bAA7E9@G6wpeOc_sf}M#rTJF#xH6+;zd9?Z+izda%C}i+xs#aG9&Y%IPoCMVqYwP?CT1;5*x~T zPlT`EHcVyim;VXI>||CfBn#xu$qJJ%i6H<_}0it5c!QolPXQ~jtI_=l@Ec1RYY_KEqPGW zK%%#=Q>Hs{>1Lwv%qp9eRn}8O%8wQiU0`r+iU<^yrq-TVVpzo=ptJ8ToZ2j zahy*U$GA2?#0lXD&>o41@X0Q7Ev!OR??fww3qDd8Q3#q0l#wb2J&La0}w3^;D=W1to+q&K)OOsBb zYS!1Y<|I3wBp`b-R}cE%0fV`Sd0~cfmniqpoHEbn1qE zB`I1t$>nKx2D|BK6JfSm!$%GJyqfp^<32y>hqg=O-vd}NHxvMlAj6Rr*A!MEt8eeR zN2&MHW8Ip$5S_}6pOs$(G)`{|=>Uj?c@~LQe4-ts`aFa{Z1h~OAgYbqQRAm`8f$|mN8S2FA5MO2OHKcHcB@oXLl5=0I8n?%;E+N>w@l-M zQbMNrlkzto(E|tO{WkaAT~DrYmv`axRv*F5T-38G^hSGo5ezz(e7siedBsIBr`LM@ zZbp5+d4lvop;Y_2AwBJU@?J0RuW`h??h)C}W3o3MFPSBmCw4qkzF0fU2{P>eQD@Y} z#lt8=s3XfQpW#-dm+1-!r?!PC7vAnibU=$L812bIj`>R%V#Z_%krON`023lS8n6jC z!gm~wP*+9#YkX^Zj+E^ESv-?d;XK4#tkt%+fsS`sm<1b5=HhBAF8Kc9S71BLvUIXG tB+_U^qu&a=KdsvqGFSMIQcjX%6_O0V{Z`8TX~Fyv3vCPKUHCr|`WK%t2x9;M literal 13210 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 T`LHSq0fEr}k52Uew;%m4K77w* diff --git a/styles/Subway/template/event/dmzx_mchat_messages_define_icons.html b/styles/Subway/template/event/dmzx_mchat_messages_define_icons.html new file mode 100644 index 0000000..e0c5ba2 --- /dev/null +++ b/styles/Subway/template/event/dmzx_mchat_messages_define_icons.html @@ -0,0 +1 @@ + diff --git a/styles/Subway/template/event/overall_header_head_append.html b/styles/Subway/template/event/overall_header_head_append.html new file mode 100644 index 0000000..22a6a0f --- /dev/null +++ b/styles/Subway/template/event/overall_header_head_append.html @@ -0,0 +1,2 @@ + + diff --git a/styles/Subway/template/mchat_navlink.html b/styles/Subway/template/mchat_navlink.html new file mode 100644 index 0000000..c313366 --- /dev/null +++ b/styles/Subway/template/mchat_navlink.html @@ -0,0 +1 @@ +

  • {MCHAT_TITLE}
  • diff --git a/styles/Subway/theme/mchat_custom.css b/styles/Subway/theme/mchat_custom.css new file mode 100644 index 0000000..7f475ec --- /dev/null +++ b/styles/Subway/theme/mchat_custom.css @@ -0,0 +1,16 @@ +/** + * + * @package phpBB Extension - mChat + * @copyright (c) 2016 dmzx - http://www.dmzx-web.net + * @copyright (c) 2016 kasimi + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2 + * + */ + +.icon-mchat { + background: none; +} + +.icon-mchat:before, #mChat + .inner li.header dt:before { + content: '\e0e6'; +} diff --git a/styles/all/template/javascript/jquery.auto-grow-input.js b/styles/all/template/javascript/jquery.auto-grow-input.js deleted file mode 100644 index 34deeca..0000000 --- a/styles/all/template/javascript/jquery.auto-grow-input.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - jQuery autoGrowInput v1.0.3 - Copyright (c) 2014 Simon Steinberger / Pixabay - Based on stackoverflow.com/questions/931207 (James Padolsey) - GitHub: https://github.com/Pixabay/jQuery-autoGrowInput - License: http://www.opensource.org/licenses/mit-license.php -*/ - -(function($){ - var event = 'oninput' in document.createElement('input') ? 'input' : 'keydown'; - - $.fn.autoGrowInput = function(options){ - var o = $.extend({ maxWidth: 500, minWidth: 20, comfortZone: 0 }, options); - - this.each(function(){ - var input = $(this), - val = ' ', - comfortZone = (options && 'comfortZone' in options) ? o.comfortZone : parseInt(input.css('fontSize')), - span = $('').css({ - position: 'absolute', - top: -9999, - left: -9999, - width: 'auto', - fontSize: input.css('fontSize'), - fontFamily: input.css('fontFamily'), - fontWeight: input.css('fontWeight'), - letterSpacing: input.css('letterSpacing'), - textTransform: input.css('textTransform'), - whiteSpace: 'nowrap', - ariaHidden: true - }).appendTo('body'), - check = function(e){ - if (val === (val = input.val()) && e.type !== 'autogrow') return; - if (!val) val = input.attr('placeholder') || ''; - span.html(val.replace(/&/g, '&').replace(/\s/g, ' ').replace(//g, '>')); - var newWidth = span.width() + comfortZone, mw = typeof(o.maxWidth) == "function" ? o.maxWidth() : o.maxWidth; - if (newWidth > mw) newWidth = mw; - else if (newWidth < o.minWidth) newWidth = o.minWidth; - if (newWidth != input.width()) input.width(newWidth); - }; - input.on(event+'.autogrow autogrow', check); - // init on page load - check(); - }); - return this; - } -}(jQuery)); diff --git a/styles/all/template/javascript/jquery.autogrow-textarea.js b/styles/all/template/javascript/jquery.autogrow-textarea.js new file mode 100644 index 0000000..c12e45a --- /dev/null +++ b/styles/all/template/javascript/jquery.autogrow-textarea.js @@ -0,0 +1,211 @@ +// Based off https://code.google.com/p/gaequery/source/browse/trunk/src/static/scripts/jquery.autogrow-textarea.js?r=2 +// Modified by David Beck +// Mofified by kasimi (c) 2016 + +( function( factory ) { + // UMD wrapper + if ( typeof define === 'function' && define.amd ) { + // AMD + define( [ 'jquery' ], factory ); + } else if ( typeof exports !== 'undefined' ) { + // Node/CommonJS + module.exports = factory( require( 'jquery' ) ); + } else { + // Browser globals + factory( jQuery ); + } +}( function( $ ) { + + /* + * Auto-growing textareas; technique ripped from Facebook + */ + $.fn.autogrow = function(options) { + + options = $.extend( { + vertical: true, + horizontal: false, + characterSlop: 0 + }, options); + + this.filter('textarea,input').each(function() { + + var $this = $(this), + borderBox = $this.css( 'box-sizing' ) === 'border-box', + // minHeight = borderBox ? $this.outerHeight() : $this.height(), + maxHeight = $this.attr( "maxHeight" ), + minWidth = typeof( $this.attr( "minWidth" ) ) == "undefined" ? 0 : $this.attr( "minWidth" ); + + if( typeof( maxHeight ) == "undefined" ) maxHeight = 1000000; + + var shadow = $('
    ').css( { + position: 'absolute', + top: -10000, + left: -10000, + fontSize: $this.css('fontSize'), + fontFamily: $this.css('fontFamily'), + fontWeight: $this.css('fontWeight'), + lineHeight: $this.css('lineHeight'), + paddingLeft: $this.css('paddingLeft'), + paddingRight: $this.css('paddingRight'), + paddingTop: $this.css('paddingTop'), + paddingBottom: $this.css('paddingBottom'), + borderTop: $this.css('borderTop'), + borderBottom: $this.css('borderBottom'), + borderLeft: $this.css('borderLeft'), + borderRight: $this.css('borderRight'), + whiteSpace: 'pre-wrap', + resize: 'none' + } ).appendTo(document.body); + + shadow.html( 'a' ); + var characterWidth = shadow.width(); + shadow.html( '' ); + var isTextarea = $this.is('textarea'); + + var update = function( val ) { + + var times = function(string, number) { + for (var i = 0, r = ''; i < number; i ++) r += string; + return r; + }; + + if( typeof val === 'undefined' ) val = this.value; + if( val === '' && $(this).attr("placeholder") ) val = $(this).attr("placeholder"); + + if( options.vertical ) + val = val.replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n$/, '
     ') + .replace(/\n/g, '
    ') + .replace(/ {2,}/g, function(space) { return times(' ', space.length -1) + ' '; }); + else + val = escapeHtml( val ); + + //if( options.horizontal ) + // val = $.trim( val ); + + // if( $(this).prop( 'tagName' ).toUpperCase() === 'INPUT' ) + // shadow.text(val).css( "width", "auto" ); + // else + shadow.html( val ).css( "width", "auto" ); // need to use html here otherwise no way to count spaces (with html we can use  ) + + if( options.horizontal ) + { + var slopWidth = options.characterSlop * characterWidth + 2; + + var newWidth = Math.max( shadow.width() + slopWidth, minWidth ); + var maxWidth = options.maxWidth; + //if( typeof( maxWidth ) === "undefined" ) maxWidth = $this.parent().width() - 12; // not sure why we were doing this but seems like a bad idea. doesn't work with inline-block parents for one thing, since it is the text area that should be "pushing" them to be wider + if( maxWidth ) newWidth = Math.min( newWidth, maxWidth ); + // Take scrollbar into account + if (isTextarea && shadow.get(0).scrollHeight > shadow.height()) { + newWidth += 20; + } + $(this).css( "width", newWidth ); + } + + if( options.vertical ) + { + var shadowWidth = $(this).width(); + if( ! borderBox ) shadowWidth = shadowWidth - parseInt($this.css('paddingLeft'),10) - parseInt($this.css('paddingRight'),10); + shadow.css( "width", shadowWidth ); + var shadowHeight = borderBox ? shadow.outerHeight() : shadow.height(); + + $(this).css( "height", "auto" ); + minHeight = borderBox ? $this.outerHeight() : $this.height(); + + var newHeight = Math.min( Math.max( shadowHeight, minHeight ), maxHeight ); + $(this).css( "height", newHeight ); + $(this).css( "overflow", newHeight == maxHeight ? "auto" : "hidden" ); + } + }; + + $(this) + .change(function(){update.call( this );return true;}) + .keyup(function(){update.call( this );return true;}) + .keypress(function( event ) { + if( event.ctrlKey || event.metaKey ) return; + + var val = this.value; + var caretInfo = _getCaretInfo( this ); + + var typedChar = event.which === 13 ? "\n" : String.fromCharCode( event.which ); + var valAfterKeypress = val.slice( 0, caretInfo.start ) + typedChar + val.slice( caretInfo.end ); + update.call( this, valAfterKeypress ); + return true; + }) + .bind( "update.autogrow", function(){ update.apply(this); } ) + .bind( "remove.autogrow", function() { + shadow.remove(); + } ); + + update.apply(this); + + }); + + return this; + }; + + // comes from https://github.com/madapaja/jquery.selection/blob/master/src/jquery.selection.js + var _getCaretInfo = function(element){ + var res = { + text: '', + start: 0, + end: 0 + }; + + if (!element.value) { + /* no value or empty string */ + return res; + } + + try { + if (window.getSelection) { + /* except IE */ + res.start = element.selectionStart; + res.end = element.selectionEnd; + res.text = element.value.slice(res.start, res.end); + } else if (doc.selection) { + /* for IE */ + element.focus(); + + var range = doc.selection.createRange(), + range2 = doc.body.createTextRange(); + + res.text = range.text; + + try { + range2.moveToElementText(element); + range2.setEndPoint('StartToStart', range); + } catch (e) { + range2 = element.createTextRange(); + range2.setEndPoint('StartToStart', range); + } + + res.start = element.value.length - range2.text.length; + res.end = res.start + range.text.length; + } + } catch (e) { + /* give up */ + } + + return res; + }; + + var entityMap = { + "&": "&", + "<": "<", + ">": ">", + '"': '"', + "'": ''', + "/": '/', + " ": ' ' + }; + + function escapeHtml(string) { + return String(string).replace(/[&<>"'\/\ ]/g, function (s) { + return entityMap[s]; + } ); + } +} ) ); diff --git a/styles/all/template/javascript/js.cookie-2.0.4.min.js b/styles/all/template/javascript/js.cookie-2.0.4.min.js deleted file mode 100644 index e5ea49e..0000000 --- a/styles/all/template/javascript/js.cookie-2.0.4.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! js-cookie v2.0.4 | MIT */ -!function(a){if("function"==typeof define&&define.amd)define(a);else if("object"==typeof exports)module.exports=a();else{var b=window.Cookies,c=window.Cookies=a();c.noConflict=function(){return window.Cookies=b,c}}}(function(){function a(){for(var a=0,b={};a1){if(f=a({path:"/"},d.defaults,f),"number"==typeof f.expires){var h=new Date;h.setMilliseconds(h.getMilliseconds()+864e5*f.expires),f.expires=h}try{g=JSON.stringify(e),/^[\{\[]/.test(g)&&(e=g)}catch(i){}return e=encodeURIComponent(String(e)),e=e.replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),b=encodeURIComponent(String(b)),b=b.replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent),b=b.replace(/[\(\)]/g,escape),document.cookie=[b,"=",e,f.expires&&"; expires="+f.expires.toUTCString(),f.path&&"; path="+f.path,f.domain&&"; domain="+f.domain,f.secure?"; secure":""].join("")}b||(g={});for(var j=document.cookie?document.cookie.split("; "):[],k=/(%[0-9A-Z]{2})+/g,l=0;l mChat.mssgLngth) { - alert(mChat.mssgLngthLong); + phpbb.alert(mChat.lang.err, mChat.lang.mssgLngthLong); return; } mChat.cached('add').prop('disabled', true); mChat.pauseSession(); - mChat.lastInputValue = mChat.cached('input').val(); - mChat.cached('input').val('').keyup().trigger('autogrow'); - mChat.refresh(mChat.lastInputValue).done(function() { + var originalInputValue = mChat.cached('input').val(); + var inputValue = originalInputValue; + var color = localStorage.getItem(mChat.cookie + 'mchat_color'); + if (color && inputValue.indexOf('[color=') === -1) { + inputValue = '[color=#' + color + '] ' + inputValue + ' [/color]'; + } + mChat.cached('input').val(''); + if (mChat.showCharCount) { + mChat.updateCharCount(); + } + mChat.refresh(inputValue).done(function() { mChat.resetSession(); }).fail(function() { - mChat.cached('input').val(mChat.lastInputValue).keyup().trigger('autogrow'); + mChat.cached('input').val(originalInputValue); + if (mChat.showCharCount) { + mChat.updateCharCount(); + } }).always(function() { mChat.cached('add').prop('disabled', false); setTimeout(function() { @@ -149,90 +237,65 @@ jQuery(function($) { }); }, edit: function() { - var $container = $(this).closest('.mchat-message'); - var $message = mChat.cached('confirm').find('textarea').show().val($container.data('mchat-message')); - mChat.cached('confirm').find('p').text(mChat.editInfo); - phpbb.confirm(mChat.cached('confirm'), function() { - ajaxRequest('edit', true, { - message_id: $container.data('mchat-id'), - message: $message.val(), - archive: mChat.archivePage ? 1 : 0 - }).done(function(json) { - mChat.updateMessages($(json.edit)); - mChat.resetSession(); - }); + var $message = $(this).closest('.mchat-message'); + mChat.confirm({ + container: mChat.cached('confirm'), + fields: function($container) { + return [ + $container.find('p').text(mChat.lang.editInfo), + $container.find('textarea').val($message.data('mchat-message')) + ]; + }, + confirm: function($p, $textarea) { + mChat.ajaxRequest('edit', true, { + message_id: $message.data('mchat-id'), + message: $textarea.val(), + archive: mChat.archivePage ? 1 : 0 + }).done(function(json) { + mChat.updateMessages($(json.edit)); + mChat.resetSession(); + }); + } }); }, del: function() { - var $container = $(this).closest('.mchat-message'); - mChat.cached('confirm').find('textarea').hide(); - mChat.cached('confirm').find('p').text(mChat.delConfirm); - phpbb.confirm(mChat.cached('confirm'), function() { - var delId = $container.data('mchat-id'); - ajaxRequest('del', true, { - message_id: delId - }).done(function() { - mChat.removeMessages([delId]); - mChat.resetSession(); - }); + var delId = $(this).closest('.mchat-message').data('mchat-id'); + mChat.confirm({ + container: mChat.cached('confirm'), + fields: function($container) { + return [ + $container.find('p').text(mChat.lang.delConfirm) + ]; + }, + confirm: function($p) { + mChat.ajaxRequest('del', true, { + message_id: delId + }).done(function() { + mChat.removeMessages([delId]); + mChat.resetSession(); + }); + } }); }, refresh: function(message) { if (mChat.isPaused && !message) { return false; } - var $messages = mChat.cached('messages').children(); var data = { - message_last_id: mChat.messageIds.length ? mChat.messageIds.max() : 0 + last: mChat.messageIds.length ? mChat.messageIds.max() : 0 }; if (message) { data.message = message; } if (mChat.liveUpdates) { - data.message_first_id = mChat.messageIds.length ? mChat.messageIds.min() : 0; - data.message_edits = {}; - var now = Math.floor(Date.now() / 1000); - $.each($messages, function() { - var $message = $(this); - var editTime = $message.data('mchat-edit-time'); - if (editTime && (!mChat.editDeleteLimit || $message.data('mchat-message-time') >= now - mChat.editDeleteLimit / 1000)) { - data.message_edits[$message.data('mchat-id')] = editTime; - } - }); + data.log = mChat.logId; } mChat.cached('status-ok', 'status-error', 'status-paused').hide(); mChat.cached('status-load').show(); - return ajaxRequest(message ? 'add' : 'refresh', !!message, data).done(function(json) { + return mChat.ajaxRequest(message ? 'add' : 'refresh', !!message, data).done(function(json) { + $(mChat).trigger('mchat_response_handle_data_before', [json]); if (json.add) { - var $html = $(json.add); - $('.mchat-no-messages').remove(); - $html.reverse(mChat.messageTop).hide().each(function(i) { - var $message = $(this); - if ($.inArray($message.data('mchat-id'), mChat.messageIds) !== -1) { - return; - } - mChat.messageIds.push($message.data('mchat-id')); - setTimeout(function() { - if (mChat.messageTop) { - mChat.cached('messages').prepend($message); - } else { - mChat.cached('messages').append($message); - } - $message.css('opacity', 0).slideDown().animate({opacity: 1}, {queue: false}); - mChat.cached('messages').animate({scrollTop: mChat.messageTop ? 0 : mChat.cached('messages')[0].scrollHeight}); - }, i * 400); - if (mChat.editDeleteLimit && $message.data('mchat-edit-delete-limit') && $message.find('[data-mchat-action="edit"], [data-mchat-action="del"]').length > 0) { - var id = $message.prop('id'); - setTimeout(function() { - $('#' + id).find('[data-mchat-action="edit"], [data-mchat-action="del"]').fadeOut(function() { - $(this).closest('li').remove(); - }); - }, mChat.editDeleteLimit); - } - mChat.startRelativeTimeUpdate($message); - }); - mChat.sound('add'); - mChat.notice(); + mChat.addMessages($(json.add)); } if (json.edit) { mChat.updateMessages($(json.edit)); @@ -241,12 +304,16 @@ jQuery(function($) { mChat.removeMessages(json.del); } if (json.whois) { - mChat.whois(); + mChat.handleWhoisResponse(json); + } + if (json.log) { + mChat.logId = json.log; } if (mChat.refreshInterval) { mChat.cached('status-load', 'status-error', 'status-paused').hide(); mChat.cached('status-ok').show(); } + $(mChat).trigger('mchat_response_handle_data_after', [json]); }); }, whois: function() { @@ -254,51 +321,133 @@ jQuery(function($) { mChat.cached('refresh-pending').show(); mChat.cached('refresh-explain').hide(); } - ajaxRequest('whois', false, {}).done(function(json) { - var $whois = $(json.whois); - var $userlist = $whois.find('#mchat-userlist'); - if (Cookies.get('mchat_show_userlist')) { - $userlist.show(); + mChat.ajaxRequest('whois', false, {}).done(mChat.handleWhoisResponse); + }, + handleWhoisResponse: function(json) { + var $whois = $(json.whois); + var $userlist = $whois.find('#mchat-userlist'); + if (localStorage.getItem(mChat.cookie + 'mchat_show_userlist')) { + $userlist.show(); + } + mChat.cached('whois').replaceWith($whois); + mChat.cache.whois = $whois; + mChat.cache.userlist = $userlist; + if (mChat.customPage) { + mChat.cached('refresh-pending').hide(); + mChat.cached('refresh-explain').show(); + } + if (json.navlink) { + $('.mchat-nav-link').html(json.navlink); + } + if (json.navlink_title) { + $('.mchat-nav-link-title').prop('title', json.navlink_title); + } + }, + addMessages: function($messages) { + var playSound = true; + mChat.cached('messages').find('.mchat-no-messages').remove(); + $messages.reverse(mChat.messageTop).hide().each(function(i) { + var $message = $(this); + var data = { + message: $message, + delay: mChat.refreshInterval ? 400 : 0, + abort: $.inArray($message.data('mchat-id'), mChat.messageIds) !== -1, + playSound: playSound + }; + $(mChat).trigger('mchat_add_message_before', [data]); + if (data.abort) { + return; } - mChat.cached('whois').replaceWith($whois); - mChat.cache.whois = $whois; - mChat.cache.userlist = $userlist; - if (mChat.customPage) { - mChat.cached('refresh-pending').hide(); - mChat.cached('refresh-explain').show(); + if (data.playSound) { + mChat.sound('add'); + mChat.titleAlert(); + playSound = false; } + mChat.messageIds.push($message.data('mchat-id')); + setTimeout(function() { + var $container = mChat.cached('messages'); + var data = { + container: $container, + message: $message, + add: function() { + if (mChat.messageTop) { + this.container.prepend(this.message); + } else { + this.container.append(this.message); + } + }, + show: function() { + var scrollTop, scrollHeight = mChat.messageTop ? 0 : $container.get(0).scrollHeight; + if (mChat.messageTop && (scrollTop = this.container.scrollTop()) > 0) { + this.message.show(); + this.container.scrollTop(scrollTop + this.message.outerHeight()); + } else { + this.message.css('opacity', 0).slideDown('fast').animate({opacity: 1}, {duration: 'fast', queue: false}); + } + if (!mChat.messageTop && this.container.scrollTop() >= scrollHeight - this.container.height()) { + this.container.animate({ + scrollTop: scrollHeight, + easing: 'swing', + duration: 'slow' + }); + } + } + }; + $(mChat).trigger('mchat_add_message_animate_before', [data]); + data.add(); + data.show(); + }, i * data.delay); + if (mChat.editDeleteLimit && $message.data('mchat-edit-delete-limit') && $message.find('[data-mchat-action="edit"], [data-mchat-action="del"]').length > 0) { + var id = $message.prop('id'); + setTimeout(function() { + $('#' + id).find('[data-mchat-action="edit"], [data-mchat-action="del"]').fadeOut(function() { + $(this).closest('li').remove(); + }); + }, mChat.editDeleteLimit); + } + mChat.startRelativeTimeUpdate($message); }); }, updateMessages: function($messages) { - var soundPlayed = false; + var playSound = true; $messages.each(function() { var $newMessage = $(this); - var $oldMessage = $('#mchat-message-' + $newMessage.data('mchat-id')); - mChat.stopRelativeTimeUpdate($oldMessage); - mChat.startRelativeTimeUpdate($newMessage); - $oldMessage.fadeOut(function() { - $oldMessage.replaceWith($newMessage.hide().fadeIn()); + var data = { + newMessage: $newMessage, + oldMessage: $('#mchat-message-' + $newMessage.data('mchat-id')), + playSound: playSound + }; + $(mChat).trigger('mchat_edit_message_before', [data]); + mChat.stopRelativeTimeUpdate(data.oldMessage); + mChat.startRelativeTimeUpdate(data.newMessage); + data.oldMessage.fadeOut(function() { + data.oldMessage.replaceWith(data.newMessage.hide().fadeIn()); }); - if (!soundPlayed) { - soundPlayed = true; + if (data.playSound) { mChat.sound('edit'); + playSound = false; } }); }, removeMessages: function(ids) { - var soundPlayed = false; + var playSound = true; $.each(ids, function(i, id) { - var index = 0; - while ((index = $.inArray(id, mChat.messageIds, index)) !== -1) { - mChat.messageIds.splice(index, 1); - var $message = $('#mchat-message-' + id); - mChat.stopRelativeTimeUpdate($message); - $message.fadeOut(function() { - $message.remove(); - }); - if (!soundPlayed) { - soundPlayed = true; + if (mChat.messageIds.removeValue(id)) { + var data = { + id: id, + message: $('#mchat-message-' + id), + playSound: playSound + }; + $(mChat).trigger('mchat_delete_message_before', [data]); + mChat.stopRelativeTimeUpdate(data.message); + (function($message) { + $message.fadeOut(function() { + $message.remove(); + }); + })(data.message); + if (data.playSound) { mChat.sound('del'); + playSound = false; } } }); @@ -318,7 +467,7 @@ jQuery(function($) { }, relativeTimeUpdate: function($time) { var minutesAgo = $time.data('mchat-minutes-ago') + 1; - var langMinutesAgo = mChat.minutesAgo[minutesAgo]; + var langMinutesAgo = mChat.lang.minutesAgo[minutesAgo]; if (langMinutesAgo) { $time.text(langMinutesAgo).data('mchat-minutes-ago', minutesAgo); } else { @@ -335,14 +484,14 @@ jQuery(function($) { }, countDown: function() { mChat.sessionTime -= 1; - mChat.cached('session').html(mChat.sessEnds.format({timeleft: mChat.timeLeft(mChat.sessionTime)})); + mChat.cached('session').html(mChat.lang.sessEnds.format({timeleft: mChat.timeLeft(mChat.sessionTime)})); if (mChat.sessionTime < 1) { mChat.endSession(); } }, pauseSession: function() { clearInterval(mChat.refreshInterval); - if (mChat.userTimeout) { + if (mChat.timeout) { clearInterval(mChat.sessionCountdown); } if (mChat.whoisRefresh) { @@ -353,10 +502,10 @@ jQuery(function($) { if (!mChat.archivePage) { clearInterval(mChat.refreshInterval); mChat.refreshInterval = setInterval(mChat.refresh, mChat.refreshTime); - if (mChat.userTimeout) { - mChat.sessionTime = mChat.userTimeout / 1000; + if (mChat.timeout) { + mChat.sessionTime = mChat.timeout / 1000; clearInterval(mChat.sessionCountdown); - mChat.cached('session').html(mChat.sessEnds.format({timeleft: mChat.timeLeft(mChat.sessionTime)})); + mChat.cached('session').html(mChat.lang.sessEnds.format({timeleft: mChat.timeLeft(mChat.sessionTime)})); mChat.sessionCountdown = setInterval(mChat.countDown, 1000); } if (mChat.whoisRefresh) { @@ -365,15 +514,15 @@ jQuery(function($) { } mChat.cached('status-ok').show(); mChat.cached('status-load', 'status-error', 'status-paused').hide(); - mChat.cached('refresh-text').html(mChat.refreshYes); + mChat.cached('refresh-text').html(mChat.lang.refreshYes); } }, endSession: function(skipUpdateWhois) { clearInterval(mChat.refreshInterval); mChat.refreshInterval = false; - if (mChat.userTimeout) { + if (mChat.timeout) { clearInterval(mChat.sessionCountdown); - mChat.cached('session').html(mChat.sessOut); + mChat.cached('session').html(mChat.lang.sessOut); } if (mChat.whoisRefresh) { clearInterval(mChat.whoisInterval); @@ -383,23 +532,31 @@ jQuery(function($) { } mChat.cached('status-load', 'status-ok', 'status-error').hide(); mChat.cached('status-paused').show(); - mChat.cached('refresh-text').html(mChat.refreshNo); + mChat.cached('refresh-text').html(mChat.lang.refreshNo); }, pauseStart: function() { mChat.isPaused = true; - mChat.cached('refresh-text').html(mChat.refreshNo); + mChat.cached('refresh-text').html(mChat.lang.refreshNo); mChat.cached('status-load', 'status-ok', 'status-error').hide(); mChat.cached('status-paused').show(); }, pauseEnd: function() { - mChat.cached('refresh-text').html(mChat.refreshYes); + mChat.cached('refresh-text').html(mChat.lang.refreshYes); mChat.cached('status-load', 'status-error', 'status-paused').hide(); mChat.cached('status-ok').show(); mChat.isPaused = false; }, + updateCharCount: function() { + var count = mChat.cached('input').val().length; + var charCount = mChat.lang.charCount.format({current: count, max: mChat.mssgLngth}); + var $elem = mChat.cached('character-count').html(charCount).toggleClass('hidden', count === 0); + if (mChat.mssgLngth) { + $elem.toggleClass('error', count > mChat.mssgLngth); + } + }, mention: function() { var $container = $(this).closest('.mchat-message'); - var username = mChat.entityDecode($container.data('mchat-username')); + var username = $container.data('mchat-username'); var usercolor = $container.data('mchat-usercolor'); if (usercolor) { username = '[b][color=' + usercolor + ']' + username + '[/color][/b]'; @@ -410,32 +567,19 @@ jQuery(function($) { }, quote: function() { var $container = $(this).closest('.mchat-message'); - var username = mChat.entityDecode($container.data('mchat-username')); - var quote = mChat.entityDecode($container.data('mchat-message')); + var username = $container.data('mchat-username'); + var quote = $container.data('mchat-message'); insert_text('[quote="' + username + '"] ' + quote + '[/quote]'); }, like: function() { var $container = $(this).closest('.mchat-message'); - var username = mChat.entityDecode($container.data('mchat-username')); - var quote = mChat.entityDecode($container.data('mchat-message')); - insert_text(mChat.likes + '[quote="' + username + '"] ' + quote + '[/quote]'); + var username = $container.data('mchat-username'); + var quote = $container.data('mchat-message'); + insert_text('[i]' + mChat.lang.likes + '[/i][quote="' + username + '"] ' + quote + '[/quote]'); }, ip: function() { popup(this.href, 750, 500); }, - entityDecode: function(text) { - var s = decodeURIComponent(text.toString().replace(/\+/g, ' ')); - s = s.replace(/</g, '<'); - s = s.replace(/>/g, '>'); - s = s.replace(/:/g, ':'); - s = s.replace(/./g, '.'); - s = s.replace(/&/g, '&'); - s = s.replace(/"/g, "'"); - return s; - }, - inputMessageLength: function() { - return $.trim(mChat.cached('input').val()).replace(/\[\/?[^\[\]]+\]/g, '').length; - }, cached: function() { return $($.map(arguments, function(name) { if (!mChat.cache[name]) { @@ -449,8 +593,6 @@ jQuery(function($) { }); mChat.cache = {}; - mChat.cached('confirm').detach().show(); - mChat.messageIds = mChat.cached('messages').children().map(function() { return $(this).data('mchat-id'); }).get(); @@ -469,44 +611,43 @@ jQuery(function($) { mChat.cached('messages').animate({scrollTop: mChat.cached('messages')[0].scrollHeight, easing: 'swing', duration: 'slow'}); } - if (!mChat.cached('user-sound').prop('checked')) { - Cookies.set('mchat_no_sound', 'yes'); - } - - mChat.cached('user-sound').prop('checked', mChat.playSound && !Cookies.get('mchat_no_sound')).change(function() { + mChat.cached('user-sound').prop('checked', mChat.playSound && !localStorage.getItem(mChat.cookie + 'mchat_no_sound')).change(function() { if (this.checked) { - Cookies.remove('mchat_no_sound'); + localStorage.removeItem(mChat.cookie + 'mchat_no_sound'); } else { - Cookies.set('mchat_no_sound', 'yes'); + localStorage.setItem(mChat.cookie + 'mchat_no_sound', 'yes'); } - }); + }).change(); $.each(mChat.removeBBCodes.split('|'), function(i, bbcode) { - $('#format-buttons .bbcode-' + bbcode).remove(); + var bbCodeClass = '.bbcode-' + bbcode.replaceMany({ + '=': '-', + '*': 'asterisk' + }); + $('#format-buttons').find(bbCodeClass).remove(); }); var $colourPalette = $('#colour_palette'); $colourPalette.appendTo($colourPalette.parent()).wrap('
    ').show(); - $('#bbpalette,#abbc3_bbpalette').prop('onclick', null).attr('data-mchat-toggle', 'colour'); + $('#bbpalette,#abbc3_bbpalette,#color_wheel').prop('onclick', null).attr('data-mchat-toggle', 'colour'); $.each(['userlist', 'smilies', 'bbcodes', 'colour'], function(i, elem) { - if (Cookies.get('mchat_show_' + elem)) { + if (localStorage.getItem(mChat.cookie + 'mchat_show_' + elem)) { mChat.cached(elem).toggle(); } }); - if (mChat.cached('input').is('input')) { - mChat.cached('form').keypress(function(e) { - if (e.which == 13) { - mChat.add(); - e.preventDefault(); - e.stopImmediatePropagation(); - } - }); - } + mChat.isTextarea = mChat.cached('input').is('textarea'); + mChat.cached('form').submit(function(e){ + e.preventDefault(); + }).keypress(function(e) { + if ((e.which == 10 || e.which == 13) && (!mChat.isTextarea || e.ctrlKey || e.metaKey) && mChat.cached('input').is(e.target)) { + mChat.add(); + } + }); if (mChat.pause) { - mChat.cached('form').keyup(function(e) { + mChat.cached('form').on('input', function() { if (mChat.refreshInterval !== false) { var val = mChat.cached('input').val(); if (mChat.isPaused && val === '') { @@ -519,21 +660,12 @@ jQuery(function($) { } if (mChat.showCharCount) { - mChat.cached('form').keyup(function(e) { - var count = mChat.inputMessageLength(); - var $elem = mChat.cached('character-count'); - $elem.html(mChat.charCount.format({current: count, max: mChat.mssgLngth})).css('visibility', count > 0 ? 'visible' : 'hidden'); - if (mChat.mssgLngth) { - $elem.toggleClass('error', count > mChat.mssgLngth); - } - }); + mChat.cached('form').on('input', mChat.updateCharCount); } - mChat.cached('form').one('keypress', function() { - mChat.cached('input').autoGrowInput({ - minWidth: mChat.cached('input').width(), - maxWidth: mChat.cached('form').width() - (mChat.cached('input').outerWidth(true) - mChat.cached('input').width()) - }); + mChat.cached('input').autogrow({ + vertical: false, + horizontal: true }); } @@ -543,13 +675,34 @@ jQuery(function($) { mChat.pageIsUnloading = true; }); + mChat.cached('colour').find('.colour-palette').on('click', 'a', function(e) { + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + e.stopImmediatePropagation(); + var $this = $(this); + var newColor = $this.data('color'); + if (localStorage.getItem(mChat.cookie + 'mchat_color') === newColor) { + localStorage.removeItem(mChat.cookie + 'mchat_color'); + } else { + localStorage.setItem(mChat.cookie + 'mchat_color', newColor); + mChat.cached('colour').find('.colour-palette a').removeClass('remember-color'); + } + $this.toggleClass('remember-color'); + } + }); + + var color = localStorage.getItem(mChat.cookie + 'mchat_color'); + if (color) { + mChat.cached('colour').find('.colour-palette a[data-color="' + color + '"]').addClass('remember-color'); + } + $('#phpbb').on('click', '[data-mchat-action]', function(e) { + e.preventDefault(); var action = $(this).data('mchat-action'); mChat[action].call(this); - e.preventDefault(); }).on('click', '[data-mchat-toggle]', function(e) { + e.preventDefault(); var elem = $(this).data('mchat-toggle'); mChat.toggle(elem); - e.preventDefault(); }); }); diff --git a/styles/prosilver/template/mchat_script_data.html b/styles/all/template/mchat_script_data.html similarity index 51% rename from styles/prosilver/template/mchat_script_data.html rename to styles/all/template/mchat_script_data.html index 7866495..df1fcfc 100644 --- a/styles/prosilver/template/mchat_script_data.html +++ b/styles/all/template/mchat_script_data.html @@ -1,3 +1,11 @@ + + + + + + + + diff --git a/styles/basic/template/mchat_navlink.html b/styles/basic/template/mchat_navlink.html index 99a90ef..b5d77c4 100644 --- a/styles/basic/template/mchat_navlink.html +++ b/styles/basic/template/mchat_navlink.html @@ -1 +1 @@ -
  • {L_MCHAT_TITLE}
  • +
  • {MCHAT_TITLE}
  • diff --git a/styles/basic/theme/mchat_custom.css b/styles/basic/theme/mchat_custom.css index dadd756..e1f9145 100644 --- a/styles/basic/theme/mchat_custom.css +++ b/styles/basic/theme/mchat_custom.css @@ -8,32 +8,32 @@ */ .icon-mchat { - position: relative; + position: relative; } .icon-mchat:after { - content: '\f086'; - font-family: 'FontAwesome'; - width: 18px; - text-align: center; - position: absolute; - top: 50%; - left: 0; - height: 14px; - margin-top: -7px; - font-size: 12px; - line-height: 14px; - vertical-align: baseline; - font-weight: normal; - font-style: normal; - text-transform: none; - text-indent: 0; - pointer-events: none; + content: '\f086'; + font-family: 'FontAwesome'; + width: 18px; + text-align: center; + position: absolute; + top: 50%; + left: 0; + height: 14px; + margin-top: -7px; + font-size: 12px; + line-height: 14px; + vertical-align: baseline; + font-weight: normal; + font-style: normal; + text-transform: none; + text-indent: 0; + pointer-events: none; } .navbar .nav-tabs .mchat .nav-link { position: relative; - text-indent: 999px; + text-indent: 999px; width: 15px; padding: 0 8px; overflow: hidden; @@ -41,7 +41,7 @@ .navbar .nav-tabs .mchat .nav-link:after { content: '\f086'; - font-family: 'FontAwesome'; + font-family: 'FontAwesome'; position: absolute; top: 50%; margin-top: -7.5px; @@ -51,18 +51,18 @@ font-weight: normal; font-style: normal; text-indent: 0; - text-align: center; + text-align: center; font-size: 15px; left:8px } .rtl .navbar .nav-tabs .mchat .nav-link { - padding-left: 12px; + padding-left: 12px; padding-right: 30px; } .rtl .navbar .nav-tabs .mchat .nav-link:after { - left: auto; + left: auto; right: 8px; } diff --git a/styles/black/template/event/dmzx_mchat_messages_define_icons.html b/styles/black/template/event/dmzx_mchat_messages_define_icons.html new file mode 100644 index 0000000..4be4b10 --- /dev/null +++ b/styles/black/template/event/dmzx_mchat_messages_define_icons.html @@ -0,0 +1 @@ + diff --git a/styles/black/template/mchat_navlink.html b/styles/black/template/mchat_navlink.html index 99a90ef..b5d77c4 100644 --- a/styles/black/template/mchat_navlink.html +++ b/styles/black/template/mchat_navlink.html @@ -1 +1 @@ -
  • {L_MCHAT_TITLE}
  • +
  • {MCHAT_TITLE}
  • diff --git a/styles/black/theme/mchat_custom.css b/styles/black/theme/mchat_custom.css index c5fb0fa..f4f5c3b 100644 --- a/styles/black/theme/mchat_custom.css +++ b/styles/black/theme/mchat_custom.css @@ -65,7 +65,3 @@ left: auto; right: 8px; } - -.mchat-button:before { - background-image: url("./images/message_icons.png"); -} diff --git a/styles/canvas/template/event/overall_footer_after.html b/styles/canvas/template/event/overall_footer_after.html new file mode 100644 index 0000000..c2080a8 --- /dev/null +++ b/styles/canvas/template/event/overall_footer_after.html @@ -0,0 +1,3 @@ + + + diff --git a/styles/canvas/template/event/overall_header_head_append.html b/styles/canvas/template/event/overall_header_head_append.html new file mode 100644 index 0000000..22a6a0f --- /dev/null +++ b/styles/canvas/template/event/overall_header_head_append.html @@ -0,0 +1,2 @@ + + diff --git a/styles/canvas/theme/mchat_custom.css b/styles/canvas/theme/mchat_custom.css new file mode 100644 index 0000000..332b32f --- /dev/null +++ b/styles/canvas/theme/mchat_custom.css @@ -0,0 +1,28 @@ +/** + * + * @package phpBB Extension - mChat + * @copyright (c) 2016 dmzx - http://www.dmzx-web.net + * @copyright (c) 2016 kasimi + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2 + * + */ + +ul#mchat-messages.topiclist li { + padding: 0; +} + +.mchat-text { + font-size: 1.1em; +} + +ul.mchat-buttons > li { + background: none !important; +} + +#mchat-panel.cp-mini .button2 { + float: none; +} + +#mchat-panel #st_editor_buttons { + display: block !important; +} diff --git a/styles/elegance/template/mchat_navlink.html b/styles/elegance/template/mchat_navlink.html index 99a90ef..b5d77c4 100644 --- a/styles/elegance/template/mchat_navlink.html +++ b/styles/elegance/template/mchat_navlink.html @@ -1 +1 @@ -
  • {L_MCHAT_TITLE}
  • +
  • {MCHAT_TITLE}
  • diff --git a/styles/latte/template/mchat_navlink.html b/styles/latte/template/mchat_navlink.html index 99a90ef..b5d77c4 100644 --- a/styles/latte/template/mchat_navlink.html +++ b/styles/latte/template/mchat_navlink.html @@ -1 +1 @@ -
  • {L_MCHAT_TITLE}
  • +
  • {MCHAT_TITLE}
  • diff --git a/styles/metro_blue/template/mchat_navlink.html b/styles/metro_blue/template/mchat_navlink.html index b84592e..5e7bd48 100644 --- a/styles/metro_blue/template/mchat_navlink.html +++ b/styles/metro_blue/template/mchat_navlink.html @@ -1 +1 @@ -
  • {L_MCHAT_TITLE}
  • +
  • {MCHAT_TITLE}
  • diff --git a/styles/pbtech/template/mchat_navlink.html b/styles/pbtech/template/mchat_navlink.html index 3dedc11..cc5ce5b 100644 --- a/styles/pbtech/template/mchat_navlink.html +++ b/styles/pbtech/template/mchat_navlink.html @@ -1 +1 @@ -
  • {L_MCHAT_TITLE}
  • +
  • {MCHAT_TITLE}
  • diff --git a/styles/pbtech/theme/mchat_custom.css b/styles/pbtech/theme/mchat_custom.css index 11b638d..bb93233 100644 --- a/styles/pbtech/theme/mchat_custom.css +++ b/styles/pbtech/theme/mchat_custom.css @@ -12,7 +12,7 @@ font-family: 'FontAwesome'; } -#mchat-panel.cp-mini { +#mchat-body .cp-mini { background-color: #E5E4E3; } diff --git a/styles/pbwow3/template/event/dmzx_mchat_messages_define_icons.html b/styles/pbwow3/template/event/dmzx_mchat_messages_define_icons.html new file mode 100644 index 0000000..4be4b10 --- /dev/null +++ b/styles/pbwow3/template/event/dmzx_mchat_messages_define_icons.html @@ -0,0 +1 @@ + diff --git a/styles/pbwow3/template/mchat_navlink.html b/styles/pbwow3/template/mchat_navlink.html index 3dedc11..cc5ce5b 100644 --- a/styles/pbwow3/template/mchat_navlink.html +++ b/styles/pbwow3/template/mchat_navlink.html @@ -1 +1 @@ -
  • {L_MCHAT_TITLE}
  • +
  • {MCHAT_TITLE}
  • diff --git a/styles/pbwow3/theme/images/message_icons.png b/styles/pbwow3/theme/images/message_icons.png deleted file mode 100644 index 18b2fd9970b19c0ca4cd2200dde3d566bfdff34b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1939 zcmV;E2WP)000yS1^@s63Y0cZ000MCNkl zQj)HcvJwGH(v=b-;5Lj?7$-2+XRKnp#<-R7guZu{U=U?pt&Ac{KgQdP*BLd86*>Ai zknw_3Hi0p{gJau}u~eToV*I)$6_Z533dT&vCXBNf?=zYh<&55pV;Gk(_GWz831hGa z5F+8ljLUO=VFRCi6MSZ*^O^L<0O&=zD#wOMY=KmVr)hQvUF$s z!PtQDdrrMTu-0Sr`5}_^U1Q=B5DW1U$wJ?wWot8fkXS~umSNJc=p#u8npcwCuaAU( zGu9UH5Bp$Cy`)$?lu8y?pu494h z!KJp}jfA{I8G{%?a}Y?pvDwDO&d zb@csk>-X`D0~n8zY$pvQUa=+PfQbs*Zdwwe-cFRA@H;ZzOXw5tFEK_~Wj`@Cw%+?O zzH-`)W;~YR`_Nfm#+4+*gZFevo+OPVhMz@1>I#aJ+sHeNU7a#GmZ2%z<6EaPuA_pn z_+%F2WX58<&?ev(j5&<$NUXI%2ZI<8Zojt<7|Mh#tq-eCySR`+s`zb5h_?&l7Ls_bnd07R zs_OjlNImp>1!H7}u?=I)WlUq7sbx2i#FhB%%}9F~>H^j6a&7l430JUNfsku41~aZ< z?4j-27_d!1#8@EQ^Nf2*2sxbEk^>XOLRITWdusi`877|O`RwO|{E4oJSO@FQ-7_m4oL_Ra2SbOsw#2$!4;SnZS00z*Ork>U*VoZe8v=g7*jE+*DkT#Fc|e?MZq#>pkQR!2 zz>bp7^kEYnZ7w1)3Zp>eC!b_oWqm6~Kv%GBjHI>%bOnT*=NY4sm{B7C2G39(s8b9A zPSUnU5Oxhy{mj>S8}WrFwxAu-N^hxN&LcV4HIQu6wIn%Z#P99$pEd!peZ0u{fh73u z;WWTPKTQ%$)@j+%I_a9g06T+xrk|um#WrUQ0x`)#=l84xgn%4aR38CxusF;!Mk6r- z!b2hj(kD=-vk8c}bQxt6q}+Z>Q4c%L%!G1;F+2Dg*#tz)cOMCx5&MC$2$+#~!~TvF zui4_}=x<0dAUCz#(|;^3%yRpYcB^+K4*{{Lo06Y^uQ2wH%r`dHgGoHb8Qb|1(ZwPlwjwNIjDTm81n`dp>KFpfqCz}- zC`noec}2upILX2Z#0ZE3!=i-pIx3J3VZ{(n+lG|-hH=0_A(uQQR@o(>$^VU2CHn}3 zlmL$Lp<}vxznk{T%_rxR#CCb)0>;$|N~JwpE#kvgj~_W8e&(jQVw(xp-&U`e`E Z{sB4U5BD+f5DNeR002ovPDHLkV1h!Wo-hCa diff --git a/styles/pbwow3/theme/mchat_custom.css b/styles/pbwow3/theme/mchat_custom.css index 8ca08b7..feb1594 100644 --- a/styles/pbwow3/theme/mchat_custom.css +++ b/styles/pbwow3/theme/mchat_custom.css @@ -20,7 +20,3 @@ content: ''; margin-right: 0; } - -.mchat-button:before { - background-image: url("./images/message_icons.png"); -} diff --git a/styles/prosilver/template/event/dmzx_mchat_custom_include.html b/styles/prosilver/template/event/dmzx_mchat_custom_include.html new file mode 100644 index 0000000..a893939 --- /dev/null +++ b/styles/prosilver/template/event/dmzx_mchat_custom_include.html @@ -0,0 +1,3 @@ + + + diff --git a/styles/prosilver/template/event/dmzx_mchat_messages_define_icons.html b/styles/prosilver/template/event/dmzx_mchat_messages_define_icons.html new file mode 100644 index 0000000..55f42b2 --- /dev/null +++ b/styles/prosilver/template/event/dmzx_mchat_messages_define_icons.html @@ -0,0 +1 @@ + diff --git a/styles/prosilver/template/event/index_body_block_online_append.html b/styles/prosilver/template/event/index_body_block_online_append.html index 15e343c..a53986f 100644 --- a/styles/prosilver/template/event/index_body_block_online_append.html +++ b/styles/prosilver/template/event/index_body_block_online_append.html @@ -3,5 +3,5 @@

    {L_MCHAT_WHO_IS_CHATTING}

    -

    {MCHAT_USERS_COUNT} {MCHAT_ONLINE_EXPLAIN}
    {MCHAT_USERS_LIST} +

    {MCHAT_USERS_TOTAL} {MCHAT_ONLINE_EXPLAIN}
    {MCHAT_USERS_LIST} diff --git a/styles/prosilver/template/event/overall_footer_copyright_append.html b/styles/prosilver/template/event/overall_footer_copyright_append.html index 878e6ca..a91abfc 100644 --- a/styles/prosilver/template/event/overall_footer_copyright_append.html +++ b/styles/prosilver/template/event/overall_footer_copyright_append.html @@ -1,3 +1,4 @@ -

    {MCHAT_DISPLAY_NAME} © {L_POST_BY_AUTHOR} {MCHAT_AUTHOR_HOMEPAGES}
    +
    + {MCHAT_DISPLAY_NAME} © {L_POST_BY_AUTHOR} {MCHAT_AUTHOR_HOMEPAGES} diff --git a/styles/prosilver/template/event/overall_header_navigation_append.html b/styles/prosilver/template/event/overall_header_navigation_append.html index 924f28d..a5415bd 100644 --- a/styles/prosilver/template/event/overall_header_navigation_append.html +++ b/styles/prosilver/template/event/overall_header_navigation_append.html @@ -1,3 +1,3 @@ - + diff --git a/styles/prosilver/template/mchat_body.html b/styles/prosilver/template/mchat_body.html index a4e9b40..bf6b4a9 100644 --- a/styles/prosilver/template/mchat_body.html +++ b/styles/prosilver/template/mchat_body.html @@ -2,16 +2,11 @@ - - - - - - - +
    + +
    - + -
    +
      @@ -46,9 +42,9 @@
    -
    - + +
    class="collapsible"> @@ -58,8 +54,10 @@
    + +
    + +
    + +
    diff --git a/styles/prosilver/template/mchat_header.html b/styles/prosilver/template/mchat_header.html index 68c4d52..a0a8f89 100644 --- a/styles/prosilver/template/mchat_header.html +++ b/styles/prosilver/template/mchat_header.html @@ -1,4 +1,4 @@
    {L_MCHAT_ARCHIVE_PAGE}{L_MCHAT_TITLE}{L_MCHAT_TITLE}
    -
     
    +
    diff --git a/styles/prosilver/template/mchat_messages.html b/styles/prosilver/template/mchat_messages.html index c0e346f..72d4aba 100644 --- a/styles/prosilver/template/mchat_messages.html +++ b/styles/prosilver/template/mchat_messages.html @@ -1,5 +1,8 @@ + + + -
  • data-mchat-usercolor="{mchatrow.MCHAT_USERNAME_COLOR}" data-mchat-message="{mchatrow.MCHAT_MESSAGE_EDIT}" data-mchat-message-time="{mchatrow.MCHAT_MESSAGE_TIME}" data-mchat-edit-time="{mchatrow.MCHAT_EDIT_TIME}" data-mchat-edit-delete-limit="1"> +
  • data-mchat-usercolor="{mchatrow.MCHAT_USERNAME_COLOR}" data-mchat-message="{mchatrow.MCHAT_MESSAGE_EDIT}" data-mchat-message-time="{mchatrow.MCHAT_MESSAGE_TIME}" data-mchat-edit-delete-limit="1" >
  • + + diff --git a/styles/prosilver/template/mchat_messages_icons.html b/styles/prosilver/template/mchat_messages_icons.html index 3e2db71..ca18f21 100644 --- a/styles/prosilver/template/mchat_messages_icons.html +++ b/styles/prosilver/template/mchat_messages_icons.html @@ -1,20 +1,20 @@ - - - + + + - + -
      -
    • {L_MCHAT_RESPOND}
    • -
    • {L_REPLY_WITH_QUOTE}
    • -
    • {L_MCHAT_LIKE}
    • -
    • {L_MCHAT_SEND_PM}
    • -
    • {mchatrow.MCHAT_WHOIS_USER}
    • -
    • {L_MCHAT_PERMISSIONS}
    • -
    • {L_MCHAT_EDIT}
    • -
    • {L_DELETE}
    • + diff --git a/styles/prosilver/template/mchat_navlink.html b/styles/prosilver/template/mchat_navlink.html index 462589b..b097aa1 100644 --- a/styles/prosilver/template/mchat_navlink.html +++ b/styles/prosilver/template/mchat_navlink.html @@ -1,5 +1,5 @@
    • class="small-icon icon-mchat"data-last-responsive="true"> - - class="icon fa fa-weixin" aria-hidden="true">{L_MCHAT_TITLE} + + class="icon fa fa-weixin" aria-hidden="true">{MCHAT_TITLE}
    • diff --git a/styles/prosilver/template/mchat_panel.html b/styles/prosilver/template/mchat_panel.html index a1ac411..4fc4005 100644 --- a/styles/prosilver/template/mchat_panel.html +++ b/styles/prosilver/template/mchat_panel.html @@ -4,7 +4,7 @@ -
      {MCHAT_CHARACTER_COUNT}
      +
      @@ -15,29 +15,31 @@ - + - +
      + - + - + - + - + - +
      +
      @@ -55,16 +57,20 @@
      -
      - - - - -
      - {MCHAT_REFRESH_YES} - {MCHAT_SESSION_TIMELEFT} - - © +
      diff --git a/styles/prosilver/template/mchat_whois.html b/styles/prosilver/template/mchat_whois.html index b5b2567..821073f 100644 --- a/styles/prosilver/template/mchat_whois.html +++ b/styles/prosilver/template/mchat_whois.html @@ -1,9 +1,9 @@
      - {MCHAT_USERS_COUNT} + {MCHAT_USERS_TOTAL} - {MCHAT_USERS_COUNT} + {MCHAT_USERS_TOTAL} diff --git a/styles/prosilver/theme/images/message_icons.png b/styles/prosilver/theme/images/message_icons_black.png similarity index 100% rename from styles/prosilver/theme/images/message_icons.png rename to styles/prosilver/theme/images/message_icons_black.png diff --git a/styles/black/theme/images/message_icons.png b/styles/prosilver/theme/images/message_icons_white.png similarity index 100% rename from styles/black/theme/images/message_icons.png rename to styles/prosilver/theme/images/message_icons_white.png diff --git a/styles/prosilver/theme/mchat.css b/styles/prosilver/theme/mchat.css index 4f7a480..07d8a2c 100644 --- a/styles/prosilver/theme/mchat.css +++ b/styles/prosilver/theme/mchat.css @@ -16,6 +16,11 @@ width: 0; } +#mchat-body { + overflow: hidden; + width: 100%; +} + .icon-mchat { background-image: url("./images/icon_mchat.png"); } @@ -24,10 +29,6 @@ display: none; } -#mchat-body { - width: 100% !important; -} - #mchat-confirm textarea { width: 100%; height: 100px; @@ -35,7 +36,6 @@ #mchat-messages { overflow: auto; - width: 100%; } .mchat-message-wrapper { @@ -59,8 +59,17 @@ @media only screen and (max-width: 700px), only screen and (max-device-width: 700px) { + #mchat-body { + overflow: visible; + } + .mchat-buttons > li { - padding: 0 8px !important; + padding: 0 6px !important; + } + + #mchat-input { + width: 95% !important; + margin: 5px 0 !important; } } @@ -69,38 +78,34 @@ display: block; } -.mchat-message .mchat-buttons li { +.mchat-wrapper .mchat-buttons li { opacity: .3; } -.mchat-message:hover .mchat-buttons li { +.mchat-wrapper li:hover .mchat-buttons li { opacity: .6; } -.mchat-message .mchat-buttons li:hover { +.mchat-wrapper li:hover .mchat-buttons li:hover { opacity: 1; } -.mchat-buttons { +.mchat-wrapper .mchat-buttons { float: right; list-style: none; margin-top: 1px; } -.mchat-message-wrapper .mchat-buttons > li { +.mchat-wrapper .mchat-buttons > li { float: left; margin: 0 3px; } -.mchat-button { - margin-left: 3px; -} - -.mchat-button.fa { +.mchat-wrapper .mchat-buttons .fa { font-size: 12pt; } -.mchat-button span { +.mchat-wrapper .mchat-buttons span { display: block; height: 0; overflow: hidden; @@ -108,9 +113,16 @@ width: 1px; } +.mchat-icons-black .mchat-icon:before { + background-image: url("./images/message_icons_black.png"); +} + +.mchat-icons-white .mchat-icon:before { + background-image: url("./images/message_icons_white.png"); +} + .mchat-icon:before { content: ''; - background-image: url("./images/message_icons.png"); background-repeat: no-repeat; width: 16px; height: 16px; @@ -118,14 +130,14 @@ float: right; } -.mchat-icon-mention:before { background-position: -2px -2px; } -.mchat-icon-edit:before { background-position: -22px -2px; } -.mchat-icon-pm:before { background-position: -42px -2px; } -.mchat-icon-quote:before { background-position: -62px -2px; } -.mchat-icon-like:before { background-position: -82px -2px; } -.mchat-icon-delete:before { background-position: -102px -2px; } -.mchat-icon-permissions:before { background-position: -122px -2px; } -.mchat-icon-ip:before { background-position: -142px -2px; } +.mchat-icon-mention:before { background-position: -2px -2px; } +.mchat-icon-edit:before { background-position: -22px -2px; } +.mchat-icon-pm:before { background-position: -42px -2px; } +.mchat-icon-quote:before { background-position: -62px -2px; } +.mchat-icon-like:before { background-position: -82px -2px; } +.mchat-icon-delete:before { background-position: -102px -2px; } +.mchat-icon-permissions:before { background-position: -122px -2px; } +.mchat-icon-ip:before { background-position: -142px -2px; } .mchat-text { clear: both; @@ -134,6 +146,10 @@ font-size: 1.2em; } +.mchat-notification-message .mchat-text { + font-style: italic; +} + .mchat-text strong { font-weight: bold !important; } @@ -160,12 +176,8 @@ list-style-type: circle; } -.mchat-text blockquote { - margin-bottom: 5px; -} - .mchat-text blockquote, .mchat-text .codebox, .mchat-text ul, .mchat-text ol { - margin-top: 5px; + margin-bottom: 5px; margin-left: 1em; } @@ -185,10 +197,13 @@ #mchat-character-count { float: right; - visibility: hidden; padding: 5px 0 0; } +#mchat-character-count.hidden { + visibility: hidden; +} + #mchat-panel { text-align: center; max-height: initial; @@ -205,11 +220,17 @@ #mchat-input { cursor: text; width: 50%; + min-width: 50%; + max-width: 90%; font-size: 1.1em; padding: 5px 5px 4px; margin: 5px 20px; } +textarea#mchat-input { + height: 8em; +} + #mchat-buttons { padding-bottom: 5px; } @@ -231,6 +252,10 @@ margin: 0 auto 5px; } +#mchat-bbcodes #colour_palette td a.remember-color { + box-shadow: 0 0 0 1px #F00; +} + #mchat-smilies { padding: 0; } @@ -288,6 +313,20 @@ background-image: url("./images/paused.gif"); } +.mchat-footer li { + display: inline; + padding-left: .1em; + white-space: nowrap; +} + +.mchat-footer li:before { + content: '\2022\A'; +} + +.mchat-footer li:first-child:before, .mchat-footer li:last-child:before { + content: ''; +} + #mchat-legend { clear: both; } @@ -342,5 +381,5 @@ } .hidden-category + .forabg #mchat-body .topiclist.forums { - display: block; + display: block; } diff --git a/styles/simplicity/template/mchat_navlink.html b/styles/simplicity/template/mchat_navlink.html index 99a90ef..36ed58b 100644 --- a/styles/simplicity/template/mchat_navlink.html +++ b/styles/simplicity/template/mchat_navlink.html @@ -1 +1 @@ -
    • {L_MCHAT_TITLE}
    • +
    • {MCHAT_TITLE}
    • diff --git a/ucp/ucp_mchat_info.php b/ucp/ucp_mchat_info.php index 51c3178..51da0b5 100644 --- a/ucp/ucp_mchat_info.php +++ b/ucp/ucp_mchat_info.php @@ -24,7 +24,7 @@ class ucp_mchat_info 'modes' => array( 'configuration' => array( 'title' => 'UCP_MCHAT_CONFIG', - 'auth' => 'ext_dmzx/mchat && acl_u_mchat_use', + 'auth' => 'ext_dmzx/mchat && acl_u_mchat_view', 'cat' => array('UCP_MCHAT_CONFIG'), ), ), diff --git a/ucp/ucp_mchat_module.php b/ucp/ucp_mchat_module.php index f6a1bb1..2166029 100644 --- a/ucp/ucp_mchat_module.php +++ b/ucp/ucp_mchat_module.php @@ -24,7 +24,7 @@ class ucp_mchat_module // Set template $this->tpl_name = 'ucp_mchat'; - $this->page_title = 'UCP_PROFILE_MCHAT'; + $this->page_title = 'UCP_MCHAT_CONFIG'; // Get an instance of the UCP controller and display the options $controller = $phpbb_container->get('dmzx.mchat.ucp.controller');