diff --git a/Languages/en_US/Admin.php b/Languages/en_US/Admin.php index 0710ebc8ef..a5187c6718 100644 --- a/Languages/en_US/Admin.php +++ b/Languages/en_US/Admin.php @@ -778,7 +778,9 @@ $txt['board_perms_deny'] = 'Deny'; $txt['all_boards_in_cat'] = 'All boards in this category'; -$txt['likes_like'] = 'Membergroups allowed to like posts'; +$txt['reacts_react'] = 'Membergroups allowed to react to posts'; +$txt['reactions_manage'] = 'Reactions'; +$txt['manage_reactions_desc'] = 'This area allows you to manage reactions'; $txt['mention'] = 'Membergroups allowed to mention users'; diff --git a/Languages/en_US/Alerts.php b/Languages/en_US/Alerts.php index cfd95eaf5b..7aadea9612 100644 --- a/Languages/en_US/Alerts.php +++ b/Languages/en_US/Alerts.php @@ -26,7 +26,7 @@ $txt['alert_topic_unapproved_reply'] = '{member_link} replied to your unapproved topic, {topic_msg}, in {board_msg}'; $txt['alert_msg_quote'] = '{member_link} quoted you in {msg_msg}'; $txt['alert_msg_mention'] = '{member_link} mentioned you in {msg_msg}'; -$txt['alert_msg_like'] = '{member_link} liked your post, {msg_msg}'; +$txt['alert_msg_react'] = '{member_link} reacted to your post, {msg_msg}'; $txt['alert_msg_report'] = '{member_link} reported the post {msg_msg}'; $txt['alert_msg_report_reply'] = '{member_link} replied to the report about {msg_msg}'; $txt['alert_member_report'] = '{member_link} reported the profile of {profile_msg}'; diff --git a/Languages/en_US/General.php b/Languages/en_US/General.php index 945e8854f9..6265471d2a 100644 --- a/Languages/en_US/General.php +++ b/Languages/en_US/General.php @@ -812,25 +812,29 @@ // Mentions $txt['mentions'] = 'Mentions'; -// Likes -$txt['likes'] = 'Likes'; +// Reactions. Previously "likes". +$txt['reactions'] = 'Reactions'; +// Leave these two for now - "like" will still be the default $txt['like'] = 'Like'; $txt['unlike'] = 'Unlike'; -$txt['like_success'] = 'Your content was successfully liked.'; -$txt['like_delete'] = 'Your content was successfully deleted.'; -$txt['like_insert'] = 'Your content was successfully inserted.'; -$txt['like_error'] = 'There was an error with your request.'; -$txt['like_disable'] = 'Likes feature is disabled.'; -$txt['not_valid_like_type'] = 'The liked type is not a valid type.'; -$txt['likes_count'] = '{num, plural, - one {# person likes this.} - other {# people like this.} + +// Todo - i18n this? Maybe react_{type}_success? +$txt['react_success'] = 'You succesfully reacted to the content '; +$txt['react_delete'] = 'Your content was successfully deleted.'; +$txt['react_insert'] = 'Your content was successfully inserted.'; +$txt['react_error'] = 'There was an error with your request.'; +$txt['reactions_disable'] = 'Reactions feature is disabled.'; +$txt['not_valid_react_type'] = 'The reacted type is not a valid type.'; +$txt['reactions_count'] = '{num, plural, + one {# person reacted to this.} + other {# people reacted to this.} }'; -$txt['you_likes_count'] = '{num, plural, - =0 {You like this.} - one {You and # other person like this.} - other {You and # other people like this.} +$txt['you_reactions_count'] = '{num, plural, + =0 {You reacted to this.} + one {You and # other person reacted to this.} + other {You and # other people reacted to this.} }'; +$txt['invalid_reaction'] = 'Invalid reaction ID'; $txt['report_to_mod'] = 'Report to moderator'; $txt['report_profile'] = 'Report profile of {member_name}'; diff --git a/Languages/en_US/ManagePermissions.php b/Languages/en_US/ManagePermissions.php index 3b5d5fdd09..969f650f75 100644 --- a/Languages/en_US/ManagePermissions.php +++ b/Languages/en_US/ManagePermissions.php @@ -241,9 +241,9 @@ $txt['permissionname_report_any'] = 'Report posts to the moderators'; $txt['permissionhelp_report_any'] = 'This permission adds a link to each message, allowing a user to report a post to a moderator. On reporting, all moderators on that board will receive an email with a link to the reported post and a description of the problem (as given by the reporting user).'; -$txt['permissiongroup_likes'] = 'Likes'; -$txt['permissionname_likes_like'] = 'Can like any content'; -$txt['permissionhelp_likes_like'] = 'This permission allows a user to like any content. Users are not allowed to like their own content.'; +$txt['permissiongroup_reactions'] = 'Reactions'; +$txt['permissionname_reactions_react'] = 'Can react to any content'; +$txt['permissionhelp_reactions_react'] = 'This permission allows a user to react to any content. Users are not allowed to react to their own content.'; $txt['permissiongroup_mentions'] = 'Mentions'; $txt['permissionname_mention'] = 'Mention others via @name'; diff --git a/Languages/en_US/ManageReactions.php b/Languages/en_US/ManageReactions.php new file mode 100644 index 0000000000..0df06215bf --- /dev/null +++ b/Languages/en_US/ManageReactions.php @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/Languages/en_US/ManageSettings.php b/Languages/en_US/ManageSettings.php index 79b6b866cf..184ee6a018 100644 --- a/Languages/en_US/ManageSettings.php +++ b/Languages/en_US/ManageSettings.php @@ -113,8 +113,8 @@ $txt['force_ssl_complete'] = 'Force SSL throughout the forum'; $txt['search_language'] = 'Fulltext Search Language'; -// Like settings. -$txt['enable_likes'] = 'Enable Likes'; +// Reaction settings. +$txt['enable_reacts'] = 'Enable Reactions'; // Mention settings. $txt['enable_mentions'] = 'Enable Mentions'; diff --git a/Languages/en_US/Profile.php b/Languages/en_US/Profile.php index 11658bbd12..4cbcd722d6 100644 --- a/Languages/en_US/Profile.php +++ b/Languages/en_US/Profile.php @@ -139,7 +139,7 @@ $txt['alert_board_notify'] = 'When a new topic is created in a board I follow, I normally want to know via...'; $txt['alert_msg_mention'] = 'When my @name is mentioned in a post'; $txt['alert_msg_quote'] = 'When one of my posts is quoted'; -$txt['alert_msg_like'] = 'When one of my posts is liked'; +$txt['alert_msg_react'] = 'When someoe reacts to one of my posts'; $txt['alert_unapproved_reply'] = 'When a reply is made to my unapproved topic'; $txt['alert_group_pm'] = 'Personal Messages'; $txt['alert_pm_new'] = 'When I receive a new personal message'; diff --git a/Languages/en_US/Stats.php b/Languages/en_US/Stats.php index 8a49c5aeed..017ebeb0e0 100644 --- a/Languages/en_US/Stats.php +++ b/Languages/en_US/Stats.php @@ -21,8 +21,8 @@ $txt['top_starters'] = 'Top Topic Starters'; $txt['top_time_online'] = 'Most Time Online'; $txt['stats_more_detailed'] = 'more detailed »'; -$txt['top_liked_messages'] = 'Top liked messages'; -$txt['top_liked_users'] = 'Top liked users'; +$txt['top_reacted_messages'] = 'Top reacted messages'; +$txt['top_reacted_users'] = 'Top reacted users'; $txt['average_members'] = 'Average registrations per day'; $txt['average_posts'] = 'Average posts per day'; diff --git a/Sources/Actions/Admin/ACP.php b/Sources/Actions/Admin/ACP.php index afa160dd2f..56e8d514ca 100644 --- a/Sources/Actions/Admin/ACP.php +++ b/Sources/Actions/Admin/ACP.php @@ -1,1998 +1,2011 @@ [ - 'title' => 'admin_main', - 'permission' => [ - 'admin_forum', - 'manage_permissions', - 'moderate_forum', - 'manage_membergroups', - 'manage_bans', - 'send_mail', - 'edit_news', - 'manage_boards', - 'manage_smileys', - 'manage_attachments', - ], - 'areas' => [ - 'index' => [ - 'label' => 'admin_center', - 'function' => __NAMESPACE__ . '\\Home::call', - 'icon' => 'administration', - ], - 'credits' => [ - 'label' => 'support_credits_title', - 'function' => __NAMESPACE__ . '\\Home::call', - 'icon' => 'support', + + declare(strict_types=1); + + namespace SMF\Actions\Admin; + + use SMF\ActionInterface; + use SMF\Actions\MessageIndex; + use SMF\Actions\Notify; + use SMF\ActionTrait; + use SMF\BBCodeParser; + use SMF\Cache\CacheApi; + use SMF\Config; + use SMF\Db\DatabaseApi as Db; + use SMF\ErrorHandler; + use SMF\IntegrationHook; + use SMF\Lang; + use SMF\Mail; + use SMF\Menu; + use SMF\SecurityToken; + use SMF\Theme; + use SMF\Url; + use SMF\User; + use SMF\Utils; + + /** + * This class, unpredictable as this might be, handles basic administration. + */ + class ACP implements ActionInterface + { + use ActionTrait; + + /******************* + * Public properties + *******************/ + + /** + * @var array + * + * Defines the menu structure for the admin center. + * See {@link Menu.php Menu.php} for details! + * + * The values of all 'title' and 'label' elements are Lang::$txt keys, and + * will be replaced at runtime with the values of those Lang::$txt strings. + * + * All occurrences of '{scripturl}' and '{boardurl}' in value strings will + * be replaced at runtime with the real values of Config::$scripturl and + * Config::$boardurl. + * + * In this default definition, all parts of the menu are set as enabled. + * At runtime, however, various parts may be turned on or off depending on + * the forum's saved settings. + * + * MOD AUTHORS: If your mod has just a few simple settings and doesn't need + * its own settings page, you don't need to bother adding anything to this + * menu. Instead, just use the integrate_general_mod_settings hook to add + * your settings to the general mod settings page. + * + * Alternatively, if you want to add a custom settings page for your mod, + * please use the integrate_admin_areas hook to add your settings page to + * $admin_areas['config']['areas']['modsettings']['subsections']. + */ + public array $admin_areas = [ + 'forum' => [ + 'title' => 'admin_main', + 'permission' => [ + 'admin_forum', + 'manage_permissions', + 'moderate_forum', + 'manage_membergroups', + 'manage_bans', + 'send_mail', + 'edit_news', + 'manage_boards', + 'manage_smileys', + 'manage_attachments', ], - 'news' => [ - 'label' => 'news_title', - 'function' => __NAMESPACE__ . '\\News::call', - 'icon' => 'news', - 'permission' => [ - 'edit_news', - 'send_mail', - 'admin_forum', + 'areas' => [ + 'index' => [ + 'label' => 'admin_center', + 'function' => __NAMESPACE__ . '\\Home::call', + 'icon' => 'administration', ], - 'subsections' => [ - 'editnews' => [ - 'label' => 'admin_edit_news', - 'permission' => 'edit_news', - ], - 'mailingmembers' => [ - 'label' => 'admin_newsletters', - 'permission' => 'send_mail', - ], - 'settings' => [ - 'label' => 'settings', - 'permission' => 'admin_forum', - ], + 'credits' => [ + 'label' => 'support_credits_title', + 'function' => __NAMESPACE__ . '\\Home::call', + 'icon' => 'support', ], - ], - 'packages' => [ - 'label' => 'package', - 'function' => 'SMF\\PackageManager\\PackageManager::call', - 'permission' => ['admin_forum'], - 'icon' => 'packages', - 'subsections' => [ - 'browse' => [ - 'label' => 'browse_packages', - ], - 'packageget' => [ - 'label' => 'download_packages', - 'url' => '{scripturl}?action=admin;area=packages;sa=packageget;get', + 'news' => [ + 'label' => 'news_title', + 'function' => __NAMESPACE__ . '\\News::call', + 'icon' => 'news', + 'permission' => [ + 'edit_news', + 'send_mail', + 'admin_forum', ], - 'perms' => [ - 'label' => 'package_file_perms', + 'subsections' => [ + 'editnews' => [ + 'label' => 'admin_edit_news', + 'permission' => 'edit_news', + ], + 'mailingmembers' => [ + 'label' => 'admin_newsletters', + 'permission' => 'send_mail', + ], + 'settings' => [ + 'label' => 'settings', + 'permission' => 'admin_forum', + ], ], - 'options' => [ - 'label' => 'package_settings', + ], + 'packages' => [ + 'label' => 'package', + 'function' => 'SMF\\PackageManager\\PackageManager::call', + 'permission' => ['admin_forum'], + 'icon' => 'packages', + 'subsections' => [ + 'browse' => [ + 'label' => 'browse_packages', + ], + 'packageget' => [ + 'label' => 'download_packages', + 'url' => '{scripturl}?action=admin;area=packages;sa=packageget;get', + ], + 'perms' => [ + 'label' => 'package_file_perms', + ], + 'options' => [ + 'label' => 'package_settings', + ], ], ], - ], - 'search' => [ - 'function' => __NAMESPACE__ . '\\Find::call', - 'permission' => ['admin_forum'], - 'select' => 'index', - ], - 'adminlogoff' => [ - 'label' => 'admin_logoff', - 'function' => __NAMESPACE__ . '\\EndSession::call', - 'enabled' => true, - 'icon' => 'exit', + 'search' => [ + 'function' => __NAMESPACE__ . '\\Find::call', + 'permission' => ['admin_forum'], + 'select' => 'index', + ], + 'adminlogoff' => [ + 'label' => 'admin_logoff', + 'function' => __NAMESPACE__ . '\\EndSession::call', + 'enabled' => true, + 'icon' => 'exit', + ], ], ], - ], - 'config' => [ - 'title' => 'admin_config', - 'permission' => ['admin_forum'], - 'areas' => [ - 'featuresettings' => [ - 'label' => 'modSettings_title', - 'function' => __NAMESPACE__ . '\\Features::call', - 'icon' => 'features', - 'subsections' => [ - 'basic' => [ - 'label' => 'mods_cat_features', - ], - 'bbc' => [ - 'label' => 'manageposts_bbc_settings', - ], - 'layout' => [ - 'label' => 'mods_cat_layout', - ], - 'sig' => [ - 'label' => 'signature_settings_short', - ], - 'profile' => [ - 'label' => 'custom_profile_shorttitle', - ], - 'likes' => [ - 'label' => 'likes', - ], - 'mentions' => [ - 'label' => 'mentions', - ], - 'alerts' => [ - 'label' => 'notifications', + 'config' => [ + 'title' => 'admin_config', + 'permission' => ['admin_forum'], + 'areas' => [ + 'featuresettings' => [ + 'label' => 'modSettings_title', + 'function' => __NAMESPACE__ . '\\Features::call', + 'icon' => 'features', + 'subsections' => [ + 'basic' => [ + 'label' => 'mods_cat_features', + ], + 'bbc' => [ + 'label' => 'manageposts_bbc_settings', + ], + 'layout' => [ + 'label' => 'mods_cat_layout', + ], + 'sig' => [ + 'label' => 'signature_settings_short', + ], + 'profile' => [ + 'label' => 'custom_profile_shorttitle', + ], + 'mentions' => [ + 'label' => 'mentions', + ], + 'alerts' => [ + 'label' => 'notifications', + ], ], ], - ], - 'antispam' => [ - 'label' => 'antispam_title', - 'function' => __NAMESPACE__ . '\\AntiSpam::call', - 'icon' => 'security', - ], - 'languages' => [ - 'label' => 'language_configuration', - 'function' => __NAMESPACE__ . '\\Languages::call', - 'icon' => 'languages', - 'subsections' => [ - 'edit' => [ - 'label' => 'language_edit', - ], - 'add' => [ - 'label' => 'language_add', - ], - 'settings' => [ - 'label' => 'language_settings', - ], + 'antispam' => [ + 'label' => 'antispam_title', + 'function' => __NAMESPACE__ . '\\AntiSpam::call', + 'icon' => 'security', ], - ], - 'current_theme' => [ - 'label' => 'theme_current_settings', - 'function' => __NAMESPACE__ . '\\Themes::call', - 'custom_url' => '{scripturl}?action=admin;area=theme;sa=list;th=%1$d', - 'icon' => 'current_theme', - ], - 'theme' => [ - 'label' => 'theme_admin', - 'function' => __NAMESPACE__ . '\\Themes::call', - 'custom_url' => '{scripturl}?action=admin;area=theme', - 'icon' => 'themes', - 'subsections' => [ - 'admin' => [ - 'label' => 'themeadmin_admin_title', - ], - 'list' => [ - 'label' => 'themeadmin_list_title', + 'languages' => [ + 'label' => 'language_configuration', + 'function' => __NAMESPACE__ . '\\Languages::call', + 'icon' => 'languages', + 'subsections' => [ + 'edit' => [ + 'label' => 'language_edit', + ], + 'add' => [ + 'label' => 'language_add', + ], + 'settings' => [ + 'label' => 'language_settings', + ], ], - 'reset' => [ - 'label' => 'themeadmin_reset_title', - ], - 'edit' => [ - 'label' => 'themeadmin_edit_title', + ], + 'current_theme' => [ + 'label' => 'theme_current_settings', + 'function' => __NAMESPACE__ . '\\Themes::call', + 'custom_url' => '{scripturl}?action=admin;area=theme;sa=list;th=%1$d', + 'icon' => 'current_theme', + ], + 'theme' => [ + 'label' => 'theme_admin', + 'function' => __NAMESPACE__ . '\\Themes::call', + 'custom_url' => '{scripturl}?action=admin;area=theme', + 'icon' => 'themes', + 'subsections' => [ + 'admin' => [ + 'label' => 'themeadmin_admin_title', + ], + 'list' => [ + 'label' => 'themeadmin_list_title', + ], + 'reset' => [ + 'label' => 'themeadmin_reset_title', + ], + 'edit' => [ + 'label' => 'themeadmin_edit_title', + ], ], ], - ], - 'modsettings' => [ - 'label' => 'admin_modifications', - 'function' => __NAMESPACE__ . '\\Mods::call', - 'icon' => 'modifications', - 'subsections' => [ - // MOD AUTHORS: If your mod has just a few simple - // settings and doesn't need its own settings page, you - // can use the integrate_general_mod_settings hook to - // add them to the 'general' page. - 'general' => [ - 'label' => 'mods_cat_modifications_misc', + 'modsettings' => [ + 'label' => 'admin_modifications', + 'function' => __NAMESPACE__ . '\\Mods::call', + 'icon' => 'modifications', + 'subsections' => [ + // MOD AUTHORS: If your mod has just a few simple + // settings and doesn't need its own settings page, you + // can use the integrate_general_mod_settings hook to + // add them to the 'general' page. + 'general' => [ + 'label' => 'mods_cat_modifications_misc', + ], + // MOD AUTHORS: If your mod has a custom settings page, + // use the integrate_admin_areas hook to insert it here. ], - // MOD AUTHORS: If your mod has a custom settings page, - // use the integrate_admin_areas hook to insert it here. ], ], ], - ], - 'layout' => [ - 'title' => 'layout_controls', - 'permission' => ['manage_boards', 'admin_forum', 'manage_smileys', 'manage_attachments', 'moderate_forum'], - 'areas' => [ - 'manageboards' => [ - 'label' => 'admin_boards', - 'function' => __NAMESPACE__ . '\\Boards::call', - 'icon' => 'boards', - 'permission' => ['manage_boards'], - 'subsections' => [ - 'main' => [ - 'label' => 'boards_edit', - ], - 'newcat' => [ - 'label' => 'mboards_new_cat', - ], - 'settings' => [ - 'label' => 'settings', - 'admin_forum', + 'layout' => [ + 'title' => 'layout_controls', + 'permission' => ['manage_boards', 'admin_forum', 'manage_smileys', 'manage_attachments', 'moderate_forum'], + 'areas' => [ + 'manageboards' => [ + 'label' => 'admin_boards', + 'function' => __NAMESPACE__ . '\\Boards::call', + 'icon' => 'boards', + 'permission' => ['manage_boards'], + 'subsections' => [ + 'main' => [ + 'label' => 'boards_edit', + ], + 'newcat' => [ + 'label' => 'mboards_new_cat', + ], + 'settings' => [ + 'label' => 'settings', + 'admin_forum', + ], ], ], - ], - 'postsettings' => [ - 'label' => 'manageposts', - 'function' => __NAMESPACE__ . '\\Posts::call', - 'permission' => ['admin_forum'], - 'icon' => 'posts', - 'subsections' => [ - 'posts' => [ - 'label' => 'manageposts_settings', - ], - 'censor' => [ - 'label' => 'admin_censored_words', - ], - 'topics' => [ - 'label' => 'manageposts_topic_settings', - ], - 'drafts' => [ - 'label' => 'manage_drafts', + 'postsettings' => [ + 'label' => 'manageposts', + 'function' => __NAMESPACE__ . '\\Posts::call', + 'permission' => ['admin_forum'], + 'icon' => 'posts', + 'subsections' => [ + 'posts' => [ + 'label' => 'manageposts_settings', + ], + 'censor' => [ + 'label' => 'admin_censored_words', + ], + 'topics' => [ + 'label' => 'manageposts_topic_settings', + ], + 'drafts' => [ + 'label' => 'manage_drafts', + ], ], ], - ], - 'managecalendar' => [ - 'label' => 'manage_calendar', - 'function' => __NAMESPACE__ . '\\Calendar::call', - 'icon' => 'calendar', - 'permission' => ['admin_forum'], - 'inactive' => false, - 'subsections' => [ - 'holidays' => [ - 'label' => 'manage_holidays', - 'permission' => 'admin_forum', - ], - 'import' => [ - 'label' => 'calendar_import', - 'permission' => 'admin_forum', - ], - 'settings' => [ - 'label' => 'calendar_settings', - 'permission' => 'admin_forum', + 'managecalendar' => [ + 'label' => 'manage_calendar', + 'function' => __NAMESPACE__ . '\\Calendar::call', + 'icon' => 'calendar', + 'permission' => ['admin_forum'], + 'inactive' => false, + 'subsections' => [ + 'holidays' => [ + 'label' => 'manage_holidays', + 'permission' => 'admin_forum', + ], + 'import' => [ + 'label' => 'calendar_import', + 'permission' => 'admin_forum', + ], + 'settings' => [ + 'label' => 'calendar_settings', + 'permission' => 'admin_forum', + ], ], ], - ], - 'managesearch' => [ - 'label' => 'manage_search', - 'function' => __NAMESPACE__ . '\\Search::call', - 'icon' => 'search', - 'permission' => ['admin_forum'], - 'subsections' => [ - 'weights' => [ - 'label' => 'search_weights', - ], - 'method' => [ - 'label' => 'search_method', - ], - 'settings' => [ - 'label' => 'settings', + 'managesearch' => [ + 'label' => 'manage_search', + 'function' => __NAMESPACE__ . '\\Search::call', + 'icon' => 'search', + 'permission' => ['admin_forum'], + 'subsections' => [ + 'weights' => [ + 'label' => 'search_weights', + ], + 'method' => [ + 'label' => 'search_method', + ], + 'settings' => [ + 'label' => 'settings', + ], ], ], - ], - 'smileys' => [ - 'label' => 'smileys_manage', - 'function' => __NAMESPACE__ . '\\Smileys::call', - 'icon' => 'smiley', - 'permission' => ['manage_smileys'], - 'subsections' => [ - 'editsets' => [ - 'label' => 'smiley_sets', - ], - 'addsmiley' => [ - 'label' => 'smileys_add', - 'enabled' => true, - ], - 'editsmileys' => [ - 'label' => 'smileys_edit', - 'enabled' => true, - ], - 'setorder' => [ - 'label' => 'smileys_set_order', - 'enabled' => true, - ], - 'editicons' => [ - 'label' => 'icons_edit_message_icons', - 'enabled' => true, - ], - 'settings' => [ - 'label' => 'settings', + 'smileys' => [ + 'label' => 'smileys_manage', + 'function' => __NAMESPACE__ . '\\Smileys::call', + 'icon' => 'smiley', + 'permission' => ['manage_smileys'], + 'subsections' => [ + 'editsets' => [ + 'label' => 'smiley_sets', + ], + 'addsmiley' => [ + 'label' => 'smileys_add', + 'enabled' => true, + ], + 'editsmileys' => [ + 'label' => 'smileys_edit', + 'enabled' => true, + ], + 'setorder' => [ + 'label' => 'smileys_set_order', + 'enabled' => true, + ], + 'editicons' => [ + 'label' => 'icons_edit_message_icons', + 'enabled' => true, + ], + 'settings' => [ + 'label' => 'settings', + ], ], ], - ], - 'manageattachments' => [ - 'label' => 'attachments_avatars', - 'function' => __NAMESPACE__ . '\\Attachments::call', - 'icon' => 'attachment', - 'permission' => ['manage_attachments'], - 'subsections' => [ - 'browse' => [ - 'label' => 'attachment_manager_browse', - ], - 'attachments' => [ - 'label' => 'attachment_manager_settings', - ], - 'avatars' => [ - 'label' => 'attachment_manager_avatar_settings', - ], - 'attachpaths' => [ - 'label' => 'attach_directories', - ], - 'maintenance' => [ - 'label' => 'attachment_manager_maintenance', + 'managereactions' => [ + 'label' => 'reactions_manage', + 'function' => __NAMESPACE__ . '\\Reactions::call', + 'icon' => 'like', + 'permission' => ['admin_forum'], + 'subsections' => [ + 'settings' => [ + 'label' => 'settings', + ], + 'edit' => [ + 'label' => 'reactions_manage', + ], ], ], - ], - 'sengines' => [ - 'label' => 'search_engines', - 'inactive' => false, - 'function' => __NAMESPACE__ . '\\SearchEngines::call', - 'icon' => 'engines', - 'permission' => 'admin_forum', - 'subsections' => [ - 'stats' => [ - 'label' => 'spider_stats', - ], - 'logs' => [ - 'label' => 'spider_logs', - ], - 'spiders' => [ - 'label' => 'spiders', + 'manageattachments' => [ + 'label' => 'attachments_avatars', + 'function' => __NAMESPACE__ . '\\Attachments::call', + 'icon' => 'attachment', + 'permission' => ['manage_attachments'], + 'subsections' => [ + 'browse' => [ + 'label' => 'attachment_manager_browse', + ], + 'attachments' => [ + 'label' => 'attachment_manager_settings', + ], + 'avatars' => [ + 'label' => 'attachment_manager_avatar_settings', + ], + 'attachpaths' => [ + 'label' => 'attach_directories', + ], + 'maintenance' => [ + 'label' => 'attachment_manager_maintenance', + ], ], - 'settings' => [ - 'label' => 'settings', + ], + 'sengines' => [ + 'label' => 'search_engines', + 'inactive' => false, + 'function' => __NAMESPACE__ . '\\SearchEngines::call', + 'icon' => 'engines', + 'permission' => 'admin_forum', + 'subsections' => [ + 'stats' => [ + 'label' => 'spider_stats', + ], + 'logs' => [ + 'label' => 'spider_logs', + ], + 'spiders' => [ + 'label' => 'spiders', + ], + 'settings' => [ + 'label' => 'settings', + ], ], ], ], ], - ], - 'members' => [ - 'title' => 'admin_manage_members', - 'permission' => [ - 'moderate_forum', - 'manage_membergroups', - 'manage_bans', - 'manage_permissions', - 'admin_forum', - ], - 'areas' => [ - 'viewmembers' => [ - 'label' => 'admin_users', - 'function' => __NAMESPACE__ . '\\Members::call', - 'icon' => 'members', - 'permission' => ['moderate_forum'], - 'subsections' => [ - 'all' => [ - 'label' => 'view_all_members', - ], - 'search' => [ - 'label' => 'mlist_search', - ], - ], + 'members' => [ + 'title' => 'admin_manage_members', + 'permission' => [ + 'moderate_forum', + 'manage_membergroups', + 'manage_bans', + 'manage_permissions', + 'admin_forum', ], - 'membergroups' => [ - 'label' => 'admin_groups', - 'function' => __NAMESPACE__ . '\\Membergroups::call', - 'icon' => 'membergroups', - 'permission' => ['manage_membergroups'], - 'subsections' => [ - 'index' => [ - 'label' => 'membergroups_edit_groups', - 'permission' => 'manage_membergroups', - ], - 'add' => [ - 'label' => 'membergroups_new_group', - 'permission' => 'manage_membergroups', - ], - 'settings' => [ - 'label' => 'settings', - 'permission' => 'admin_forum', + 'areas' => [ + 'viewmembers' => [ + 'label' => 'admin_users', + 'function' => __NAMESPACE__ . '\\Members::call', + 'icon' => 'members', + 'permission' => ['moderate_forum'], + 'subsections' => [ + 'all' => [ + 'label' => 'view_all_members', + ], + 'search' => [ + 'label' => 'mlist_search', + ], ], ], - ], - 'permissions' => [ - 'label' => 'edit_permissions', - 'function' => __NAMESPACE__ . '\\Permissions::call', - 'icon' => 'permissions', - 'permission' => ['manage_permissions'], - 'subsections' => [ - 'index' => [ - 'label' => 'permissions_groups', - 'permission' => 'manage_permissions', - ], - 'board' => [ - 'label' => 'permissions_boards', - 'permission' => 'manage_permissions', - ], - 'profiles' => [ - 'label' => 'permissions_profiles', - 'permission' => 'manage_permissions', + 'membergroups' => [ + 'label' => 'admin_groups', + 'function' => __NAMESPACE__ . '\\Membergroups::call', + 'icon' => 'membergroups', + 'permission' => ['manage_membergroups'], + 'subsections' => [ + 'index' => [ + 'label' => 'membergroups_edit_groups', + 'permission' => 'manage_membergroups', + ], + 'add' => [ + 'label' => 'membergroups_new_group', + 'permission' => 'manage_membergroups', + ], + 'settings' => [ + 'label' => 'settings', + 'permission' => 'admin_forum', + ], ], - 'postmod' => [ - 'label' => 'permissions_post_moderation', - 'permission' => 'manage_permissions', - ], - 'settings' => [ - 'label' => 'settings', - 'permission' => 'admin_forum', - ], - ], - ], - 'regcenter' => [ - 'label' => 'registration_center', - 'function' => __NAMESPACE__ . '\\Registration::call', - 'icon' => 'regcenter', - 'permission' => [ - 'admin_forum', - 'moderate_forum', ], - 'subsections' => [ - 'register' => [ - 'label' => 'admin_browse_register_new', - 'permission' => 'moderate_forum', - ], - 'agreement' => [ - 'label' => 'registration_agreement', - 'permission' => 'admin_forum', - ], - 'policy' => [ - 'label' => 'privacy_policy', - 'permission' => 'admin_forum', - ], - 'reservednames' => [ - 'label' => 'admin_reserved_set', - 'permission' => 'admin_forum', - ], - 'settings' => [ - 'label' => 'settings', - 'permission' => 'admin_forum', + 'permissions' => [ + 'label' => 'edit_permissions', + 'function' => __NAMESPACE__ . '\\Permissions::call', + 'icon' => 'permissions', + 'permission' => ['manage_permissions'], + 'subsections' => [ + 'index' => [ + 'label' => 'permissions_groups', + 'permission' => 'manage_permissions', + ], + 'board' => [ + 'label' => 'permissions_boards', + 'permission' => 'manage_permissions', + ], + 'profiles' => [ + 'label' => 'permissions_profiles', + 'permission' => 'manage_permissions', + ], + 'postmod' => [ + 'label' => 'permissions_post_moderation', + 'permission' => 'manage_permissions', + ], + 'settings' => [ + 'label' => 'settings', + 'permission' => 'admin_forum', + ], ], ], - ], - 'warnings' => [ - 'label' => 'warnings', - 'function' => __NAMESPACE__ . '\\Warnings::call', - 'icon' => 'warning', - 'inactive' => false, - 'permission' => ['admin_forum'], - ], - 'ban' => [ - 'label' => 'ban_title', - 'function' => __NAMESPACE__ . '\\Bans::call', - 'icon' => 'ban', - 'permission' => 'manage_bans', - 'subsections' => [ - 'list' => [ - 'label' => 'ban_edit_list', - ], - 'add' => [ - 'label' => 'ban_add_new', - ], - 'browse' => [ - 'label' => 'ban_trigger_browse', - ], - 'log' => [ - 'label' => 'ban_log', + 'regcenter' => [ + 'label' => 'registration_center', + 'function' => __NAMESPACE__ . '\\Registration::call', + 'icon' => 'regcenter', + 'permission' => [ + 'admin_forum', + 'moderate_forum', + ], + 'subsections' => [ + 'register' => [ + 'label' => 'admin_browse_register_new', + 'permission' => 'moderate_forum', + ], + 'agreement' => [ + 'label' => 'registration_agreement', + 'permission' => 'admin_forum', + ], + 'policy' => [ + 'label' => 'privacy_policy', + 'permission' => 'admin_forum', + ], + 'reservednames' => [ + 'label' => 'admin_reserved_set', + 'permission' => 'admin_forum', + ], + 'settings' => [ + 'label' => 'settings', + 'permission' => 'admin_forum', + ], ], ], - ], - 'paidsubscribe' => [ - 'label' => 'paid_subscriptions', - 'inactive' => false, - 'function' => __NAMESPACE__ . '\\Subscriptions::call', - 'icon' => 'paid', - 'permission' => 'admin_forum', - 'subsections' => [ - 'view' => [ - 'label' => 'paid_subs_view', + 'warnings' => [ + 'label' => 'warnings', + 'function' => __NAMESPACE__ . '\\Warnings::call', + 'icon' => 'warning', + 'inactive' => false, + 'permission' => ['admin_forum'], + ], + 'ban' => [ + 'label' => 'ban_title', + 'function' => __NAMESPACE__ . '\\Bans::call', + 'icon' => 'ban', + 'permission' => 'manage_bans', + 'subsections' => [ + 'list' => [ + 'label' => 'ban_edit_list', + ], + 'add' => [ + 'label' => 'ban_add_new', + ], + 'browse' => [ + 'label' => 'ban_trigger_browse', + ], + 'log' => [ + 'label' => 'ban_log', + ], ], - 'settings' => [ - 'label' => 'settings', + ], + 'paidsubscribe' => [ + 'label' => 'paid_subscriptions', + 'inactive' => false, + 'function' => __NAMESPACE__ . '\\Subscriptions::call', + 'icon' => 'paid', + 'permission' => 'admin_forum', + 'subsections' => [ + 'view' => [ + 'label' => 'paid_subs_view', + ], + 'settings' => [ + 'label' => 'settings', + ], ], ], ], ], - ], - 'maintenance' => [ - 'title' => 'admin_maintenance', - 'permission' => ['admin_forum'], - 'areas' => [ - 'serversettings' => [ - 'label' => 'admin_server_settings', - 'function' => __NAMESPACE__ . '\\Server::call', - 'icon' => 'server', - 'subsections' => [ - 'general' => [ - 'label' => 'general_settings', - ], - 'database' => [ - 'label' => 'database_settings', - ], - 'cookie' => [ - 'label' => 'cookies_sessions_settings', - ], - 'security' => [ - 'label' => 'security_settings', - ], - 'cache' => [ - 'label' => 'caching_settings', - ], - 'export' => [ - 'label' => 'export_settings', - ], - 'loads' => [ - 'label' => 'load_balancing_settings', - ], - 'phpinfo' => [ - 'label' => 'phpinfo_settings', + 'maintenance' => [ + 'title' => 'admin_maintenance', + 'permission' => ['admin_forum'], + 'areas' => [ + 'serversettings' => [ + 'label' => 'admin_server_settings', + 'function' => __NAMESPACE__ . '\\Server::call', + 'icon' => 'server', + 'subsections' => [ + 'general' => [ + 'label' => 'general_settings', + ], + 'database' => [ + 'label' => 'database_settings', + ], + 'cookie' => [ + 'label' => 'cookies_sessions_settings', + ], + 'security' => [ + 'label' => 'security_settings', + ], + 'cache' => [ + 'label' => 'caching_settings', + ], + 'export' => [ + 'label' => 'export_settings', + ], + 'loads' => [ + 'label' => 'load_balancing_settings', + ], + 'phpinfo' => [ + 'label' => 'phpinfo_settings', + ], ], ], - ], - 'maintain' => [ - 'label' => 'maintain_title', - 'function' => __NAMESPACE__ . '\\Maintenance::call', - 'icon' => 'maintain', - 'subsections' => [ - 'routine' => [ - 'label' => 'maintain_sub_routine', - 'permission' => 'admin_forum', - ], - 'database' => [ - 'label' => 'maintain_sub_database', - 'permission' => 'admin_forum', - ], - 'members' => [ - 'label' => 'maintain_sub_members', - 'permission' => 'admin_forum', - ], - 'topics' => [ - 'label' => 'maintain_sub_topics', - 'permission' => 'admin_forum', - ], - 'hooks' => [ - 'label' => 'hooks_title_list', - 'permission' => 'admin_forum', + 'maintain' => [ + 'label' => 'maintain_title', + 'function' => __NAMESPACE__ . '\\Maintenance::call', + 'icon' => 'maintain', + 'subsections' => [ + 'routine' => [ + 'label' => 'maintain_sub_routine', + 'permission' => 'admin_forum', + ], + 'database' => [ + 'label' => 'maintain_sub_database', + 'permission' => 'admin_forum', + ], + 'members' => [ + 'label' => 'maintain_sub_members', + 'permission' => 'admin_forum', + ], + 'topics' => [ + 'label' => 'maintain_sub_topics', + 'permission' => 'admin_forum', + ], + 'hooks' => [ + 'label' => 'hooks_title_list', + 'permission' => 'admin_forum', + ], ], ], - ], - 'scheduledtasks' => [ - 'label' => 'maintain_tasks', - 'function' => __NAMESPACE__ . '\\Tasks::call', - 'icon' => 'scheduled', - 'subsections' => [ - 'tasks' => [ - 'label' => 'maintain_tasks', - 'permission' => 'admin_forum', - ], - 'tasklog' => [ - 'label' => 'scheduled_log', - 'permission' => 'admin_forum', - ], - 'settings' => [ - 'label' => 'scheduled_tasks_settings', - 'permission' => 'admin_forum', + 'scheduledtasks' => [ + 'label' => 'maintain_tasks', + 'function' => __NAMESPACE__ . '\\Tasks::call', + 'icon' => 'scheduled', + 'subsections' => [ + 'tasks' => [ + 'label' => 'maintain_tasks', + 'permission' => 'admin_forum', + ], + 'tasklog' => [ + 'label' => 'scheduled_log', + 'permission' => 'admin_forum', + ], + 'settings' => [ + 'label' => 'scheduled_tasks_settings', + 'permission' => 'admin_forum', + ], ], ], - ], - 'mailqueue' => [ - 'label' => 'mailqueue_title', - 'function' => __NAMESPACE__ . '\\Mail::call', - 'icon' => 'mail', - 'subsections' => [ - 'browse' => [ - 'label' => 'mailqueue_browse', - 'permission' => 'admin_forum', - ], - 'settings' => [ - 'label' => 'mailqueue_settings', - 'permission' => 'admin_forum', - ], - 'test' => [ - 'label' => 'mailqueue_test', - 'permission' => 'admin_forum', + 'mailqueue' => [ + 'label' => 'mailqueue_title', + 'function' => __NAMESPACE__ . '\\Mail::call', + 'icon' => 'mail', + 'subsections' => [ + 'browse' => [ + 'label' => 'mailqueue_browse', + 'permission' => 'admin_forum', + ], + 'settings' => [ + 'label' => 'mailqueue_settings', + 'permission' => 'admin_forum', + ], + 'test' => [ + 'label' => 'mailqueue_test', + 'permission' => 'admin_forum', + ], ], ], - ], - 'reports' => [ - 'label' => 'generate_reports', - 'function' => __NAMESPACE__ . '\\Reports::call', - 'icon' => 'reports', - ], - 'logs' => [ - 'label' => 'logs', - 'function' => __NAMESPACE__ . '\\Logs::call', - 'icon' => 'logs', - 'subsections' => [ - 'errorlog' => [ - 'label' => 'errorlog', - 'permission' => 'admin_forum', - 'enabled' => true, - 'url' => '{scripturl}?action=admin;area=logs;sa=errorlog;desc', - ], - 'adminlog' => [ - 'label' => 'admin_log', - 'permission' => 'admin_forum', - 'enabled' => true, - ], - 'modlog' => [ - 'label' => 'moderation_log', - 'permission' => 'admin_forum', - 'enabled' => true, - ], - 'banlog' => [ - 'label' => 'ban_log', - 'permission' => 'manage_bans', - ], - 'spiderlog' => [ - 'label' => 'spider_logs', - 'permission' => 'admin_forum', - 'enabled' => true, - ], - 'tasklog' => [ - 'label' => 'scheduled_log', - 'permission' => 'admin_forum', - ], - 'settings' => [ - 'label' => 'log_settings', - 'permission' => 'admin_forum', + 'reports' => [ + 'label' => 'generate_reports', + 'function' => __NAMESPACE__ . '\\Reports::call', + 'icon' => 'reports', + ], + 'logs' => [ + 'label' => 'logs', + 'function' => __NAMESPACE__ . '\\Logs::call', + 'icon' => 'logs', + 'subsections' => [ + 'errorlog' => [ + 'label' => 'errorlog', + 'permission' => 'admin_forum', + 'enabled' => true, + 'url' => '{scripturl}?action=admin;area=logs;sa=errorlog;desc', + ], + 'adminlog' => [ + 'label' => 'admin_log', + 'permission' => 'admin_forum', + 'enabled' => true, + ], + 'modlog' => [ + 'label' => 'moderation_log', + 'permission' => 'admin_forum', + 'enabled' => true, + ], + 'banlog' => [ + 'label' => 'ban_log', + 'permission' => 'manage_bans', + ], + 'spiderlog' => [ + 'label' => 'spider_logs', + 'permission' => 'admin_forum', + 'enabled' => true, + ], + 'tasklog' => [ + 'label' => 'scheduled_log', + 'permission' => 'admin_forum', + ], + 'settings' => [ + 'label' => 'log_settings', + 'permission' => 'admin_forum', + ], ], ], - ], - 'repairboards' => [ - 'label' => 'admin_repair', - 'function' => __NAMESPACE__ . '\\RepairBoards::call', - 'select' => 'maintain', - 'hidden' => true, + 'repairboards' => [ + 'label' => 'admin_repair', + 'function' => __NAMESPACE__ . '\\RepairBoards::call', + 'select' => 'maintain', + 'hidden' => true, + ], ], ], - ], - ]; - - /**************** - * Public methods - ****************/ - - /** - * The main admin handling function. - * - * It initialises all the basic context required for the admin center. - * It passes execution onto the relevant admin section. - * If the passed section is not found it shows the admin home page. - */ - public function execute(): void - { - // Make sure the administrator has a valid session... - User::$me->validateSession(); - - // Actually create the menu! - // Hook call disabled because we already called it in setAdminAreas() - $menu = new Menu($this->admin_areas, [ - 'do_big_icons' => true, - 'disable_hook_call' => true, - ]); - - // Nothing valid? - if (empty($menu->include_data)) { - ErrorHandler::fatalLang('no_access', false); - } - - // Build the link tree. - Utils::$context['linktree'][] = [ - 'url' => Config::$scripturl . '?action=admin', - 'name' => Lang::$txt['admin_center'], ]; - if (isset($menu->current_area) && $menu->current_area != 'index') { - Utils::$context['linktree'][] = [ - 'url' => Config::$scripturl . '?action=admin;area=' . $menu->current_area . ';' . Utils::$context['session_var'] . '=' . Utils::$context['session_id'], - 'name' => $menu->include_data['label'], - ]; - } + /**************** + * Public methods + ****************/ + + /** + * The main admin handling function. + * + * It initialises all the basic context required for the admin center. + * It passes execution onto the relevant admin section. + * If the passed section is not found it shows the admin home page. + */ + public function execute(): void + { + // Make sure the administrator has a valid session... + User::$me->validateSession(); + + // Actually create the menu! + // Hook call disabled because we already called it in setAdminAreas() + $menu = new Menu($this->admin_areas, [ + 'do_big_icons' => true, + 'disable_hook_call' => true, + ]); + + // Nothing valid? + if (empty($menu->include_data)) { + ErrorHandler::fatalLang('no_access', false); + } - if (!empty($menu->current_subsection) && $menu->include_data['subsections'][$menu->current_subsection]['label'] != $menu->include_data['label']) { + // Build the link tree. Utils::$context['linktree'][] = [ - 'url' => Config::$scripturl . '?action=admin;area=' . $menu->current_area . ';sa=' . $menu->current_subsection . ';' . Utils::$context['session_var'] . '=' . Utils::$context['session_id'], - 'name' => $menu->include_data['subsections'][$menu->current_subsection]['label'], + 'url' => Config::$scripturl . '?action=admin', + 'name' => Lang::$txt['admin_center'], ]; - } - // Make a note of the Unique ID for this menu. - Utils::$context['admin_menu_id'] = $menu->id; - Utils::$context['admin_menu_name'] = $menu->name; + if (isset($menu->current_area) && $menu->current_area != 'index') { + Utils::$context['linktree'][] = [ + 'url' => Config::$scripturl . '?action=admin;area=' . $menu->current_area . ';' . Utils::$context['session_var'] . '=' . Utils::$context['session_id'], + 'name' => $menu->include_data['label'], + ]; + } - // Where in the admin are we? - Utils::$context['admin_area'] = $menu->current_area; + if (!empty($menu->current_subsection) && $menu->include_data['subsections'][$menu->current_subsection]['label'] != $menu->include_data['label']) { + Utils::$context['linktree'][] = [ + 'url' => Config::$scripturl . '?action=admin;area=' . $menu->current_area . ';sa=' . $menu->current_subsection . ';' . Utils::$context['session_var'] . '=' . Utils::$context['session_id'], + 'name' => $menu->include_data['subsections'][$menu->current_subsection]['label'], + ]; + } - // Now - finally - call the right place! - if (isset($menu->include_data['file'])) { - require_once Config::$sourcedir . '/' . $menu->include_data['file']; - } + // Make a note of the Unique ID for this menu. + Utils::$context['admin_menu_id'] = $menu->id; + Utils::$context['admin_menu_name'] = $menu->name; - // Get the right callable. - $call = Utils::getCallable($menu->include_data['function']); + // Where in the admin are we? + Utils::$context['admin_area'] = $menu->current_area; - // Is it valid? - if (!empty($call)) { - call_user_func($call); - } - } + // Now - finally - call the right place! + if (isset($menu->include_data['file'])) { + require_once Config::$sourcedir . '/' . $menu->include_data['file']; + } - /*********************** - * Public static methods - ***********************/ + // Get the right callable. + $call = Utils::getCallable($menu->include_data['function']); - /** - * Helper function, it sets up the context for database settings. - * - * @todo see rev. 10406 from 2.1-requests - * - * @param array $config_vars An array of configuration variables - */ - public static function prepareDBSettingContext(array &$config_vars): void - { - Lang::load('Help'); - - if (isset($_SESSION['adm-save'])) { - if ($_SESSION['adm-save'] === true) { - Utils::$context['saved_successful'] = true; - } else { - Utils::$context['saved_failed'] = $_SESSION['adm-save']; + // Is it valid? + if (!empty($call)) { + call_user_func($call); } - - unset($_SESSION['adm-save']); } - Utils::$context['config_vars'] = []; - $inlinePermissions = []; - $bbcChoice = []; - $board_list = false; - - foreach ($config_vars as $config_var) { - // HR? - if (!is_array($config_var)) { - Utils::$context['config_vars'][] = $config_var; - } else { - // If it has no name it doesn't have any purpose! - if (empty($config_var[1])) { - continue; - } - - // Special case for inline permissions - if ($config_var[0] == 'permissions' && User::$me->allowedTo('manage_permissions')) { - $inlinePermissions[] = $config_var[1]; - } elseif ($config_var[0] == 'permissions') { - continue; - } - - if ($config_var[0] == 'boards') { - $board_list = true; + /*********************** + * Public static methods + ***********************/ + + /** + * Helper function, it sets up the context for database settings. + * + * @todo see rev. 10406 from 2.1-requests + * + * @param array $config_vars An array of configuration variables + */ + public static function prepareDBSettingContext(array &$config_vars): void + { + Lang::load('Help'); + + if (isset($_SESSION['adm-save'])) { + if ($_SESSION['adm-save'] === true) { + Utils::$context['saved_successful'] = true; + } else { + Utils::$context['saved_failed'] = $_SESSION['adm-save']; } - // Are we showing the BBC selection box? - if ($config_var[0] == 'bbc') { - $bbcChoice[] = $config_var[1]; - } + unset($_SESSION['adm-save']); + } - // We need to do some parsing of the value before we pass it in. - if (isset(Config::$modSettings[$config_var[1]])) { - switch ($config_var[0]) { - case 'select': - $value = Config::$modSettings[$config_var[1]]; - break; + Utils::$context['config_vars'] = []; + $inlinePermissions = []; + $bbcChoice = []; + $board_list = false; - case 'json': - $value = Utils::htmlspecialchars(Utils::jsonEncode(Config::$modSettings[$config_var[1]])); - break; + foreach ($config_vars as $config_var) { + // HR? + if (!is_array($config_var)) { + Utils::$context['config_vars'][] = $config_var; + } else { + // If it has no name it doesn't have any purpose! + if (empty($config_var[1])) { + continue; + } - case 'boards': - $value = explode(',', Config::$modSettings[$config_var[1]]); - break; + // Special case for inline permissions + if ($config_var[0] == 'permissions' && User::$me->allowedTo('manage_permissions')) { + $inlinePermissions[] = $config_var[1]; + } elseif ($config_var[0] == 'permissions') { + continue; + } - default: - $value = Utils::htmlspecialchars((string) Config::$modSettings[$config_var[1]]); + if ($config_var[0] == 'boards') { + $board_list = true; } - } else { - // Darn, it's empty. What type is expected? - switch ($config_var[0]) { - case 'int': - case 'float': - $value = 0; - break; - case 'select': - $value = !empty($config_var['multiple']) ? Utils::jsonEncode([]) : ''; - break; + // Are we showing the BBC selection box? + if ($config_var[0] == 'bbc') { + $bbcChoice[] = $config_var[1]; + } - case 'boards': - $value = []; - break; + // We need to do some parsing of the value before we pass it in. + if (isset(Config::$modSettings[$config_var[1]])) { + switch ($config_var[0]) { + case 'select': + $value = Config::$modSettings[$config_var[1]]; + break; - default: - $value = ''; - } - } + case 'json': + $value = Utils::htmlspecialchars(Utils::jsonEncode(Config::$modSettings[$config_var[1]])); + break; - Utils::$context['config_vars'][$config_var[1]] = [ - 'label' => $config_var['text_label'] ?? (Lang::$txt[$config_var[1]] ?? (isset($config_var[3]) && !is_array($config_var[3]) ? $config_var[3] : '')), - 'help' => isset(Lang::$helptxt[$config_var[1]]) ? $config_var[1] : '', - 'type' => $config_var[0], - 'size' => !empty($config_var['size']) ? $config_var['size'] : (!empty($config_var[2]) && !is_array($config_var[2]) ? $config_var[2] : (in_array($config_var[0], ['int', 'float']) ? 6 : 0)), - 'data' => [], - 'name' => $config_var[1], - 'value' => $value, - 'disabled' => false, - 'invalid' => !empty($config_var['invalid']), - 'javascript' => '', - 'var_message' => !empty($config_var['message']) && isset(Lang::$txt[$config_var['message']]) ? Lang::$txt[$config_var['message']] : '', - 'preinput' => $config_var['preinput'] ?? '', - 'postinput' => $config_var['postinput'] ?? '', - ]; + case 'boards': + $value = explode(',', Config::$modSettings[$config_var[1]]); + break; - // Handle min/max/step if necessary - if ($config_var[0] == 'int' || $config_var[0] == 'float') { - // Default to a min of 0 if one isn't set - if (isset($config_var['min'])) { - Utils::$context['config_vars'][$config_var[1]]['min'] = $config_var['min']; + default: + $value = Utils::htmlspecialchars((string) Config::$modSettings[$config_var[1]]); + } } else { - Utils::$context['config_vars'][$config_var[1]]['min'] = 0; - } + // Darn, it's empty. What type is expected? + switch ($config_var[0]) { + case 'int': + case 'float': + $value = 0; + break; - if (isset($config_var['max'])) { - Utils::$context['config_vars'][$config_var[1]]['max'] = $config_var['max']; - } + case 'select': + $value = !empty($config_var['multiple']) ? Utils::jsonEncode([]) : ''; + break; + + case 'boards': + $value = []; + break; - if (isset($config_var['step'])) { - Utils::$context['config_vars'][$config_var[1]]['step'] = $config_var['step']; + default: + $value = ''; + } } - } - // If this is a select box handle any data. - if (!empty($config_var[2]) && is_array($config_var[2])) { - // If we allow multiple selections, we need to adjust a few things. - if ($config_var[0] == 'select' && !empty($config_var['multiple'])) { - Utils::$context['config_vars'][$config_var[1]]['name'] .= '[]'; + Utils::$context['config_vars'][$config_var[1]] = [ + 'label' => $config_var['text_label'] ?? (Lang::$txt[$config_var[1]] ?? (isset($config_var[3]) && !is_array($config_var[3]) ? $config_var[3] : '')), + 'help' => isset(Lang::$helptxt[$config_var[1]]) ? $config_var[1] : '', + 'type' => $config_var[0], + 'size' => !empty($config_var['size']) ? $config_var['size'] : (!empty($config_var[2]) && !is_array($config_var[2]) ? $config_var[2] : (in_array($config_var[0], ['int', 'float']) ? 6 : 0)), + 'data' => [], + 'name' => $config_var[1], + 'value' => $value, + 'disabled' => false, + 'invalid' => !empty($config_var['invalid']), + 'javascript' => '', + 'var_message' => !empty($config_var['message']) && isset(Lang::$txt[$config_var['message']]) ? Lang::$txt[$config_var['message']] : '', + 'preinput' => $config_var['preinput'] ?? '', + 'postinput' => $config_var['postinput'] ?? '', + ]; - Utils::$context['config_vars'][$config_var[1]]['value'] = !empty(Utils::$context['config_vars'][$config_var[1]]['value']) ? Utils::jsonDecode(Utils::$context['config_vars'][$config_var[1]]['value'], true) : []; - } + // Handle min/max/step if necessary + if ($config_var[0] == 'int' || $config_var[0] == 'float') { + // Default to a min of 0 if one isn't set + if (isset($config_var['min'])) { + Utils::$context['config_vars'][$config_var[1]]['min'] = $config_var['min']; + } else { + Utils::$context['config_vars'][$config_var[1]]['min'] = 0; + } - // If it's associative - if (isset($config_var[2][0]) && is_array($config_var[2][0])) { - Utils::$context['config_vars'][$config_var[1]]['data'] = $config_var[2]; - } else { - foreach ($config_var[2] as $key => $item) { - Utils::$context['config_vars'][$config_var[1]]['data'][] = [$key, $item]; + if (isset($config_var['max'])) { + Utils::$context['config_vars'][$config_var[1]]['max'] = $config_var['max']; } - } - if (empty($config_var['size']) && !empty($config_var['multiple'])) { - Utils::$context['config_vars'][$config_var[1]]['size'] = max(4, count($config_var[2])); + if (isset($config_var['step'])) { + Utils::$context['config_vars'][$config_var[1]]['step'] = $config_var['step']; + } } - } - // Finally allow overrides - and some final cleanups. - foreach ($config_var as $k => $v) { - if (!is_numeric($k)) { - if (str_starts_with($k, 'on')) { - Utils::$context['config_vars'][$config_var[1]]['javascript'] .= ' ' . $k . '="' . $v . '"'; + // If this is a select box handle any data. + if (!empty($config_var[2]) && is_array($config_var[2])) { + // If we allow multiple selections, we need to adjust a few things. + if ($config_var[0] == 'select' && !empty($config_var['multiple'])) { + Utils::$context['config_vars'][$config_var[1]]['name'] .= '[]'; + + Utils::$context['config_vars'][$config_var[1]]['value'] = !empty(Utils::$context['config_vars'][$config_var[1]]['value']) ? Utils::jsonDecode(Utils::$context['config_vars'][$config_var[1]]['value'], true) : []; + } + + // If it's associative + if (isset($config_var[2][0]) && is_array($config_var[2][0])) { + Utils::$context['config_vars'][$config_var[1]]['data'] = $config_var[2]; } else { - Utils::$context['config_vars'][$config_var[1]][$k] = $v; + foreach ($config_var[2] as $key => $item) { + Utils::$context['config_vars'][$config_var[1]]['data'][] = [$key, $item]; + } + } + + if (empty($config_var['size']) && !empty($config_var['multiple'])) { + Utils::$context['config_vars'][$config_var[1]]['size'] = max(4, count($config_var[2])); } } - // See if there are any other labels that might fit? - if (isset(Lang::$txt['setting_' . $config_var[1]])) { - Utils::$context['config_vars'][$config_var[1]]['label'] = Lang::$txt['setting_' . $config_var[1]]; - } elseif (isset(Lang::$txt['groups_' . $config_var[1]])) { - Utils::$context['config_vars'][$config_var[1]]['label'] = Lang::$txt['groups_' . $config_var[1]]; + // Finally allow overrides - and some final cleanups. + foreach ($config_var as $k => $v) { + if (!is_numeric($k)) { + if (str_starts_with($k, 'on')) { + Utils::$context['config_vars'][$config_var[1]]['javascript'] .= ' ' . $k . '="' . $v . '"'; + } else { + Utils::$context['config_vars'][$config_var[1]][$k] = $v; + } + } + + // See if there are any other labels that might fit? + if (isset(Lang::$txt['setting_' . $config_var[1]])) { + Utils::$context['config_vars'][$config_var[1]]['label'] = Lang::$txt['setting_' . $config_var[1]]; + } elseif (isset(Lang::$txt['groups_' . $config_var[1]])) { + Utils::$context['config_vars'][$config_var[1]]['label'] = Lang::$txt['groups_' . $config_var[1]]; + } } - } - // Set the subtext in case it's part of the label. - // @todo Temporary. Preventing divs inside label tags. - $divPos = strpos(Utils::$context['config_vars'][$config_var[1]]['label'], ']*>~', '', substr(Utils::$context['config_vars'][$config_var[1]]['label'], $divPos)); + if ($divPos !== false) { + Utils::$context['config_vars'][$config_var[1]]['subtext'] = preg_replace('~]*>~', '', substr(Utils::$context['config_vars'][$config_var[1]]['label'], $divPos)); - Utils::$context['config_vars'][$config_var[1]]['label'] = substr(Utils::$context['config_vars'][$config_var[1]]['label'], 0, $divPos); + Utils::$context['config_vars'][$config_var[1]]['label'] = substr(Utils::$context['config_vars'][$config_var[1]]['label'], 0, $divPos); + } } } - } - // If we have inline permissions we need to prep them. - if (!empty($inlinePermissions) && User::$me->allowedTo('manage_permissions')) { - Permissions::init_inline_permissions($inlinePermissions); - } + // If we have inline permissions we need to prep them. + if (!empty($inlinePermissions) && User::$me->allowedTo('manage_permissions')) { + Permissions::init_inline_permissions($inlinePermissions); + } - if ($board_list) { - Utils::$context['board_list'] = MessageIndex::getBoardList(); - } + if ($board_list) { + Utils::$context['board_list'] = MessageIndex::getBoardList(); + } - // What about any BBC selection boxes? - if (!empty($bbcChoice)) { - // What are the options, eh? - $temp = BBCodeParser::getCodes(); - $bbcTags = []; + // What about any BBC selection boxes? + if (!empty($bbcChoice)) { + // What are the options, eh? + $temp = BBCodeParser::getCodes(); + $bbcTags = []; - foreach ($temp as $tag) { - if (!isset($tag['require_parents'])) { - $bbcTags[] = $tag['tag']; + foreach ($temp as $tag) { + if (!isset($tag['require_parents'])) { + $bbcTags[] = $tag['tag']; + } } - } - $bbcTags = array_unique($bbcTags); + $bbcTags = array_unique($bbcTags); - // The number of columns we want to show the BBC tags in. - $numColumns = Utils::$context['num_bbc_columns'] ?? 3; + // The number of columns we want to show the BBC tags in. + $numColumns = Utils::$context['num_bbc_columns'] ?? 3; - // Now put whatever BBC options we may have into context too! - Utils::$context['bbc_sections'] = []; + // Now put whatever BBC options we may have into context too! + Utils::$context['bbc_sections'] = []; - foreach ($bbcChoice as $bbcSection) { - Utils::$context['bbc_sections'][$bbcSection] = [ - 'title' => Lang::$txt['bbc_title_' . $bbcSection] ?? Lang::$txt['enabled_bbc_select'], - 'disabled' => empty(Config::$modSettings['bbc_disabled_' . $bbcSection]) ? [] : Config::$modSettings['bbc_disabled_' . $bbcSection], - 'all_selected' => empty(Config::$modSettings['bbc_disabled_' . $bbcSection]), - 'columns' => [], - ]; + foreach ($bbcChoice as $bbcSection) { + Utils::$context['bbc_sections'][$bbcSection] = [ + 'title' => Lang::$txt['bbc_title_' . $bbcSection] ?? Lang::$txt['enabled_bbc_select'], + 'disabled' => empty(Config::$modSettings['bbc_disabled_' . $bbcSection]) ? [] : Config::$modSettings['bbc_disabled_' . $bbcSection], + 'all_selected' => empty(Config::$modSettings['bbc_disabled_' . $bbcSection]), + 'columns' => [], + ]; - if ($bbcSection == 'legacyBBC') { - $sectionTags = array_intersect(Utils::$context['legacy_bbc'], $bbcTags); - } else { - $sectionTags = array_diff($bbcTags, Utils::$context['legacy_bbc']); - } + if ($bbcSection == 'legacyBBC') { + $sectionTags = array_intersect(Utils::$context['legacy_bbc'], $bbcTags); + } else { + $sectionTags = array_diff($bbcTags, Utils::$context['legacy_bbc']); + } - $totalTags = count($sectionTags); - $tagsPerColumn = ceil($totalTags / $numColumns); + $totalTags = count($sectionTags); + $tagsPerColumn = ceil($totalTags / $numColumns); - $col = 0; - $i = 0; + $col = 0; + $i = 0; - foreach ($sectionTags as $tag) { - if ($i % $tagsPerColumn == 0 && $i != 0) { - $col++; - } + foreach ($sectionTags as $tag) { + if ($i % $tagsPerColumn == 0 && $i != 0) { + $col++; + } - Utils::$context['bbc_sections'][$bbcSection]['columns'][$col][] = [ - 'tag' => $tag, - 'show_help' => isset(Lang::$helptxt['tag_' . $tag]), - ]; + Utils::$context['bbc_sections'][$bbcSection]['columns'][$col][] = [ + 'tag' => $tag, + 'show_help' => isset(Lang::$helptxt['tag_' . $tag]), + ]; - $i++; + $i++; + } } } + + IntegrationHook::call('integrate_prepare_db_settings', [&$config_vars]); + SecurityToken::create('admin-dbsc'); } - IntegrationHook::call('integrate_prepare_db_settings', [&$config_vars]); - SecurityToken::create('admin-dbsc'); - } + /** + * Helper function. Saves settings by putting them in Settings.php or saving them in the settings table. + * + * - Saves those settings set from ?action=admin;area=serversettings. + * - Requires the admin_forum permission. + * - Contains arrays of the types of data to save into Settings.php. + * + * @param array $config_vars An array of configuration variables + */ + public static function saveSettings(array &$config_vars): void + { + SecurityToken::validate('admin-ssc'); + + // Fix the darn stupid cookiename! (more may not be allowed, but these for sure!) + if (isset($_POST['cookiename'])) { + $_POST['cookiename'] = preg_replace('~[,;\s\.$]+~' . (Utils::$context['utf8'] ? 'u' : ''), '', $_POST['cookiename']); + } - /** - * Helper function. Saves settings by putting them in Settings.php or saving them in the settings table. - * - * - Saves those settings set from ?action=admin;area=serversettings. - * - Requires the admin_forum permission. - * - Contains arrays of the types of data to save into Settings.php. - * - * @param array $config_vars An array of configuration variables - */ - public static function saveSettings(array &$config_vars): void - { - SecurityToken::validate('admin-ssc'); + // Fix the forum's URL if necessary. + if (isset($_POST['boardurl'])) { + if (str_ends_with($_POST['boardurl'], '/index.php')) { + $_POST['boardurl'] = substr($_POST['boardurl'], 0, -10); + } elseif (str_ends_with($_POST['boardurl'], '/')) { + $_POST['boardurl'] = substr($_POST['boardurl'], 0, -1); + } - // Fix the darn stupid cookiename! (more may not be allowed, but these for sure!) - if (isset($_POST['cookiename'])) { - $_POST['cookiename'] = preg_replace('~[,;\s\.$]+~' . (Utils::$context['utf8'] ? 'u' : ''), '', $_POST['cookiename']); - } + if (!str_starts_with($_POST['boardurl'], 'http://') && !str_starts_with($_POST['boardurl'], 'file://') && !str_starts_with($_POST['boardurl'], 'https://')) { + $_POST['boardurl'] = 'http://' . $_POST['boardurl']; + } - // Fix the forum's URL if necessary. - if (isset($_POST['boardurl'])) { - if (str_ends_with($_POST['boardurl'], '/index.php')) { - $_POST['boardurl'] = substr($_POST['boardurl'], 0, -10); - } elseif (str_ends_with($_POST['boardurl'], '/')) { - $_POST['boardurl'] = substr($_POST['boardurl'], 0, -1); + $_POST['boardurl'] = (string) new Url($_POST['boardurl'], true); } - if (!str_starts_with($_POST['boardurl'], 'http://') && !str_starts_with($_POST['boardurl'], 'file://') && !str_starts_with($_POST['boardurl'], 'https://')) { - $_POST['boardurl'] = 'http://' . $_POST['boardurl']; - } + // Any passwords? + $config_passwords = []; - $_POST['boardurl'] = (string) new Url($_POST['boardurl'], true); - } + // All the numeric variables. + $config_nums = []; - // Any passwords? - $config_passwords = []; + // All the checkboxes + $config_bools = []; - // All the numeric variables. - $config_nums = []; + // Ones that accept multiple types (should be rare) + $config_multis = []; - // All the checkboxes - $config_bools = []; + // Get all known setting definitions and assign them to our groups above. + $settings_defs = Config::getSettingsDefs(); - // Ones that accept multiple types (should be rare) - $config_multis = []; + foreach ($settings_defs as $var => $def) { + if (!is_string($var)) { + continue; + } - // Get all known setting definitions and assign them to our groups above. - $settings_defs = Config::getSettingsDefs(); + if (!empty($def['is_password'])) { + $config_passwords[] = $var; + } else { + // Special handling if multiple types are allowed. + if (is_array($def['type'])) { + // Obviously, we don't need null here. + $def['type'] = array_filter( + $def['type'], + function ($type) { + return $type !== 'NULL'; + }, + ); + + $type = count($def['type']) == 1 ? reset($def['type']) : 'multiple'; + } else { + $type = $def['type']; + } - foreach ($settings_defs as $var => $def) { - if (!is_string($var)) { - continue; - } + switch ($type) { + case 'multiple': + $config_multis[$var] = $def['type']; + // no break - if (!empty($def['is_password'])) { - $config_passwords[] = $var; - } else { - // Special handling if multiple types are allowed. - if (is_array($def['type'])) { - // Obviously, we don't need null here. - $def['type'] = array_filter( - $def['type'], - function ($type) { - return $type !== 'NULL'; - }, - ); - - $type = count($def['type']) == 1 ? reset($def['type']) : 'multiple'; - } else { - $type = $def['type']; - } + case 'double': + $config_nums[] = $var; + break; - switch ($type) { - case 'multiple': - $config_multis[$var] = $def['type']; - // no break - - case 'double': - $config_nums[] = $var; - break; - - case 'integer': - // Some things saved as integers are presented as booleans - foreach ($config_vars as $config_var) { - if (is_array($config_var) && $config_var[0] == $var) { - if ($config_var[3] == 'check') { - $config_bools[] = $var; - break 2; + case 'integer': + // Some things saved as integers are presented as booleans + foreach ($config_vars as $config_var) { + if (is_array($config_var) && $config_var[0] == $var) { + if ($config_var[3] == 'check') { + $config_bools[] = $var; + break 2; + } + break; } - break; } - } - $config_nums[] = $var; - break; + $config_nums[] = $var; + break; - case 'boolean': - $config_bools[] = $var; - break; + case 'boolean': + $config_bools[] = $var; + break; - default: - break; + default: + break; + } } } - } - // Now sort everything into a big array, and figure out arrays and etc. - $new_settings = []; + // Now sort everything into a big array, and figure out arrays and etc. + $new_settings = []; - // Figure out which config vars we're saving here... - foreach ($config_vars as $config_var) { - if (!is_array($config_var) || $config_var[2] != 'file') { - continue; - } + // Figure out which config vars we're saving here... + foreach ($config_vars as $config_var) { + if (!is_array($config_var) || $config_var[2] != 'file') { + continue; + } - $var_name = $config_var[0]; + $var_name = $config_var[0]; - // Unknown setting? - if (!isset($settings_defs[$var_name]) && isset($config_var[3])) { - switch ($config_var[3]) { - case 'int': - case 'float': - $config_nums[] = $var_name; - break; + // Unknown setting? + if (!isset($settings_defs[$var_name]) && isset($config_var[3])) { + switch ($config_var[3]) { + case 'int': + case 'float': + $config_nums[] = $var_name; + break; - case 'check': - $config_bools[] = $var_name; - break; + case 'check': + $config_bools[] = $var_name; + break; - default: - break; + default: + break; + } } - } - if (!in_array($var_name, $config_bools) && !isset($_POST[$var_name])) { - continue; - } - - if (in_array($var_name, $config_passwords)) { - if (isset($_POST[$var_name][1]) && $_POST[$var_name][0] == $_POST[$var_name][1]) { - $new_settings[$var_name] = $_POST[$var_name][0]; + if (!in_array($var_name, $config_bools) && !isset($_POST[$var_name])) { + continue; } - } elseif (in_array($var_name, $config_nums)) { - $new_settings[$var_name] = (int) $_POST[$var_name]; - // If no min is specified, assume 0. This is done to avoid having to specify 'min => 0' for all settings where 0 is the min... - $min = $config_var['min'] ?? 0; - $new_settings[$var_name] = max($min, $new_settings[$var_name]); + if (in_array($var_name, $config_passwords)) { + if (isset($_POST[$var_name][1]) && $_POST[$var_name][0] == $_POST[$var_name][1]) { + $new_settings[$var_name] = $_POST[$var_name][0]; + } + } elseif (in_array($var_name, $config_nums)) { + $new_settings[$var_name] = (int) $_POST[$var_name]; + + // If no min is specified, assume 0. This is done to avoid having to specify 'min => 0' for all settings where 0 is the min... + $min = $config_var['min'] ?? 0; + $new_settings[$var_name] = max($min, $new_settings[$var_name]); - // Is there a max value for this as well? - if (isset($config_var['max'])) { - $new_settings[$var_name] = min($config_var['max'], $new_settings[$var_name]); - } - } elseif (in_array($var_name, $config_bools)) { - $new_settings[$var_name] = !empty($_POST[$var_name]); - } elseif (isset($config_multis[$var_name])) { - $is_acceptable_type = false; - - foreach ($config_multis[$var_name] as $type) { - $temp = $_POST[$var_name]; - settype($temp, $type); - - if ($temp == $_POST[$var_name]) { - $new_settings[$var_name] = $temp; - $is_acceptable_type = true; - break; + // Is there a max value for this as well? + if (isset($config_var['max'])) { + $new_settings[$var_name] = min($config_var['max'], $new_settings[$var_name]); + } + } elseif (in_array($var_name, $config_bools)) { + $new_settings[$var_name] = !empty($_POST[$var_name]); + } elseif (isset($config_multis[$var_name])) { + $is_acceptable_type = false; + + foreach ($config_multis[$var_name] as $type) { + $temp = $_POST[$var_name]; + settype($temp, $type); + + if ($temp == $_POST[$var_name]) { + $new_settings[$var_name] = $temp; + $is_acceptable_type = true; + break; + } } - } - if (!$is_acceptable_type) { - ErrorHandler::fatal('Invalid config_var \'' . $var_name . '\''); + if (!$is_acceptable_type) { + ErrorHandler::fatal('Invalid config_var \'' . $var_name . '\''); + } + } else { + $new_settings[$var_name] = $_POST[$var_name]; } - } else { - $new_settings[$var_name] = $_POST[$var_name]; } - } - // Save the relevant settings in the Settings.php file. - Config::updateSettingsFile($new_settings); + // Save the relevant settings in the Settings.php file. + Config::updateSettingsFile($new_settings); - // Now loop through the remaining (database-based) settings. - $new_settings = []; + // Now loop through the remaining (database-based) settings. + $new_settings = []; - foreach ($config_vars as $config_var) { - // We just saved the file-based settings, so skip their definitions. - if (!is_array($config_var) || $config_var[2] == 'file') { - continue; - } + foreach ($config_vars as $config_var) { + // We just saved the file-based settings, so skip their definitions. + if (!is_array($config_var) || $config_var[2] == 'file') { + continue; + } - $new_setting = [$config_var[3], $config_var[0]]; + $new_setting = [$config_var[3], $config_var[0]]; - // Select options need carried over, too. - if (isset($config_var[4])) { - $new_setting[] = $config_var[4]; - } + // Select options need carried over, too. + if (isset($config_var[4])) { + $new_setting[] = $config_var[4]; + } - // Include min and max if necessary - if (isset($config_var['min'])) { - $new_setting['min'] = $config_var['min']; - } + // Include min and max if necessary + if (isset($config_var['min'])) { + $new_setting['min'] = $config_var['min']; + } - if (isset($config_var['max'])) { - $new_setting['max'] = $config_var['max']; - } + if (isset($config_var['max'])) { + $new_setting['max'] = $config_var['max']; + } - // Rewrite the definition a bit. - $new_settings[] = $new_setting; - } + // Rewrite the definition a bit. + $new_settings[] = $new_setting; + } - // Save the new database-based settings, if any. - if (!empty($new_settings)) { - ACP::saveDBSettings($new_settings); + // Save the new database-based settings, if any. + if (!empty($new_settings)) { + ACP::saveDBSettings($new_settings); + } } - } - - /** - * Helper function for saving database settings. - * - * @param array $config_vars An array of configuration variables - */ - public static function saveDBSettings(array &$config_vars): void - { - static $board_list = null; - SecurityToken::validate('admin-dbsc'); + /** + * Helper function for saving database settings. + * + * @param array $config_vars An array of configuration variables + */ + public static function saveDBSettings(array &$config_vars): void + { + static $board_list = null; - $inlinePermissions = []; + SecurityToken::validate('admin-dbsc'); - foreach ($config_vars as $var) { - if (!isset($var[1]) || (!isset($_POST[$var[1]]) && $var[0] != 'check' && $var[0] != 'permissions' && $var[0] != 'boards' && ($var[0] != 'bbc' || !isset($_POST[$var[1] . '_enabledTags'])))) { - continue; - } + $inlinePermissions = []; - // Checkboxes! - if ($var[0] == 'check') { - $setArray[$var[1]] = !empty($_POST[$var[1]]) ? '1' : '0'; - } - // Select boxes! - elseif ($var[0] == 'select' && in_array($_POST[$var[1]], array_keys($var[2]))) { - $setArray[$var[1]] = $_POST[$var[1]]; - } elseif ($var[0] == 'select' && !empty($var['multiple']) && array_intersect($_POST[$var[1]], array_keys($var[2])) != []) { - // For security purposes we validate this line by line. - $lOptions = []; - - foreach ($_POST[$var[1]] as $invar) { - if (in_array($invar, array_keys($var[2]))) { - $lOptions[] = $invar; - } + foreach ($config_vars as $var) { + if (!isset($var[1]) || (!isset($_POST[$var[1]]) && $var[0] != 'check' && $var[0] != 'permissions' && $var[0] != 'boards' && ($var[0] != 'bbc' || !isset($_POST[$var[1] . '_enabledTags'])))) { + continue; } - $setArray[$var[1]] = Utils::jsonEncode($lOptions); - } - // List of boards! - elseif ($var[0] == 'boards') { - // We just need a simple list of valid boards, nothing more. - if ($board_list === null) { - $board_list = []; - $request = Db::$db->query( - '', - 'SELECT id_board - FROM {db_prefix}boards', - ); - - while ($row = Db::$db->fetch_row($request)) { - $board_list[$row[0]] = true; + // Checkboxes! + if ($var[0] == 'check') { + $setArray[$var[1]] = !empty($_POST[$var[1]]) ? '1' : '0'; + } + // Select boxes! + elseif ($var[0] == 'select' && in_array($_POST[$var[1]], array_keys($var[2]))) { + $setArray[$var[1]] = $_POST[$var[1]]; + } elseif ($var[0] == 'select' && !empty($var['multiple']) && array_intersect($_POST[$var[1]], array_keys($var[2])) != []) { + // For security purposes we validate this line by line. + $lOptions = []; + + foreach ($_POST[$var[1]] as $invar) { + if (in_array($invar, array_keys($var[2]))) { + $lOptions[] = $invar; + } } - Db::$db->free_result($request); + + $setArray[$var[1]] = Utils::jsonEncode($lOptions); } + // List of boards! + elseif ($var[0] == 'boards') { + // We just need a simple list of valid boards, nothing more. + if ($board_list === null) { + $board_list = []; + $request = Db::$db->query( + '', + 'SELECT id_board + FROM {db_prefix}boards', + ); + + while ($row = Db::$db->fetch_row($request)) { + $board_list[$row[0]] = true; + } + Db::$db->free_result($request); + } - $lOptions = []; + $lOptions = []; - if (!empty($_POST[$var[1]])) { - foreach ($_POST[$var[1]] as $invar => $dummy) { - if (isset($board_list[$invar])) { - $lOptions[] = $invar; + if (!empty($_POST[$var[1]])) { + foreach ($_POST[$var[1]] as $invar => $dummy) { + if (isset($board_list[$invar])) { + $lOptions[] = $invar; + } } } - } - $setArray[$var[1]] = !empty($lOptions) ? implode(',', $lOptions) : ''; - } - // Integers! - elseif ($var[0] == 'int') { - $setArray[$var[1]] = (int) $_POST[$var[1]]; + $setArray[$var[1]] = !empty($lOptions) ? implode(',', $lOptions) : ''; + } + // Integers! + elseif ($var[0] == 'int') { + $setArray[$var[1]] = (int) $_POST[$var[1]]; - // If no min is specified, assume 0. This is done to avoid having to specify 'min => 0' for all settings where 0 is the min... - $min = $var['min'] ?? 0; - $setArray[$var[1]] = max($min, $setArray[$var[1]]); + // If no min is specified, assume 0. This is done to avoid having to specify 'min => 0' for all settings where 0 is the min... + $min = $var['min'] ?? 0; + $setArray[$var[1]] = max($min, $setArray[$var[1]]); - // Do we have a max value for this as well? - if (isset($var['max'])) { - $setArray[$var[1]] = min($var['max'], $setArray[$var[1]]); + // Do we have a max value for this as well? + if (isset($var['max'])) { + $setArray[$var[1]] = min($var['max'], $setArray[$var[1]]); + } } - } - // Floating point! - elseif ($var[0] == 'float') { - $setArray[$var[1]] = (float) $_POST[$var[1]]; + // Floating point! + elseif ($var[0] == 'float') { + $setArray[$var[1]] = (float) $_POST[$var[1]]; - // If no min is specified, assume 0. This is done to avoid having to specify 'min => 0' for all settings where 0 is the min... - $min = $var['min'] ?? 0; - $setArray[$var[1]] = max($min, $setArray[$var[1]]); + // If no min is specified, assume 0. This is done to avoid having to specify 'min => 0' for all settings where 0 is the min... + $min = $var['min'] ?? 0; + $setArray[$var[1]] = max($min, $setArray[$var[1]]); - // Do we have a max value for this as well? - if (isset($var['max'])) { - $setArray[$var[1]] = min($var['max'], $setArray[$var[1]]); + // Do we have a max value for this as well? + if (isset($var['max'])) { + $setArray[$var[1]] = min($var['max'], $setArray[$var[1]]); + } } - } - // Text! - elseif (in_array($var[0], ['text', 'large_text', 'color', 'date', 'datetime', 'datetime-local', 'email', 'month', 'time'])) { - $setArray[$var[1]] = $_POST[$var[1]]; - } - // Passwords! - elseif ($var[0] == 'password') { - if (isset($_POST[$var[1]][1]) && $_POST[$var[1]][0] == $_POST[$var[1]][1]) { - $setArray[$var[1]] = $_POST[$var[1]][0]; + // Text! + elseif (in_array($var[0], ['text', 'large_text', 'color', 'date', 'datetime', 'datetime-local', 'email', 'month', 'time'])) { + $setArray[$var[1]] = $_POST[$var[1]]; } - } - // BBC. - elseif ($var[0] == 'bbc') { - $bbcTags = []; - - foreach (BBCodeParser::getCodes() as $tag) { - $bbcTags[] = $tag['tag']; + // Passwords! + elseif ($var[0] == 'password') { + if (isset($_POST[$var[1]][1]) && $_POST[$var[1]][0] == $_POST[$var[1]][1]) { + $setArray[$var[1]] = $_POST[$var[1]][0]; + } } + // BBC. + elseif ($var[0] == 'bbc') { + $bbcTags = []; - if (!isset($_POST[$var[1] . '_enabledTags'])) { - $_POST[$var[1] . '_enabledTags'] = []; - } elseif (!is_array($_POST[$var[1] . '_enabledTags'])) { - $_POST[$var[1] . '_enabledTags'] = [$_POST[$var[1] . '_enabledTags']]; - } + foreach (BBCodeParser::getCodes() as $tag) { + $bbcTags[] = $tag['tag']; + } + + if (!isset($_POST[$var[1] . '_enabledTags'])) { + $_POST[$var[1] . '_enabledTags'] = []; + } elseif (!is_array($_POST[$var[1] . '_enabledTags'])) { + $_POST[$var[1] . '_enabledTags'] = [$_POST[$var[1] . '_enabledTags']]; + } - $setArray[$var[1]] = implode(',', array_diff($bbcTags, $_POST[$var[1] . '_enabledTags'])); + $setArray[$var[1]] = implode(',', array_diff($bbcTags, $_POST[$var[1] . '_enabledTags'])); + } + // Permissions? + elseif ($var[0] == 'permissions') { + $inlinePermissions[] = $var[1]; + } } - // Permissions? - elseif ($var[0] == 'permissions') { - $inlinePermissions[] = $var[1]; + + if (!empty($setArray)) { + Config::updateModSettings($setArray); } - } - if (!empty($setArray)) { - Config::updateModSettings($setArray); + // If we have inline permissions we need to save them. + if (!empty($inlinePermissions) && User::$me->allowedTo('manage_permissions')) { + Permissions::save_inline_permissions($inlinePermissions); + } } - // If we have inline permissions we need to save them. - if (!empty($inlinePermissions) && User::$me->allowedTo('manage_permissions')) { - Permissions::save_inline_permissions($inlinePermissions); - } - } + /** + * Get a list of versions that are currently installed on the server. + * + * @param array $checkFor An array of what to check versions for - can contain one or more of 'gd', 'imagemagick', 'db_server', 'phpa', 'memcache', 'php' or 'server' + * @return array An array of versions (keys are same as what was in $checkFor, values are the versions) + */ + public static function getServerVersions(array $checkFor): array + { + Lang::load('Admin'); + Lang::load('ManageSettings'); + + $versions = []; + + // Is GD available? If it is, we should show version information for it too. + if (in_array('gd', $checkFor) && function_exists('gd_info')) { + $temp = gd_info(); + $versions['gd'] = ['title' => Lang::$txt['support_versions_gd'], 'version' => $temp['GD Version']]; + } - /** - * Get a list of versions that are currently installed on the server. - * - * @param array $checkFor An array of what to check versions for - can contain one or more of 'gd', 'imagemagick', 'db_server', 'phpa', 'memcache', 'php' or 'server' - * @return array An array of versions (keys are same as what was in $checkFor, values are the versions) - */ - public static function getServerVersions(array $checkFor): array - { - Lang::load('Admin'); - Lang::load('ManageSettings'); + // Why not have a look at ImageMagick? If it's installed, we should show version information for it too. + if (in_array('imagemagick', $checkFor) && class_exists('Imagick')) { + $temp = new \Imagick(); + $temp2 = $temp->getVersion(); + $im_version = $temp2['versionString']; + $extension_version = 'Imagick ' . phpversion('Imagick'); - $versions = []; + // We already know it's ImageMagick and the website isn't needed... + $im_version = str_replace(['ImageMagick ', ' https://www.imagemagick.org'], '', $im_version); - // Is GD available? If it is, we should show version information for it too. - if (in_array('gd', $checkFor) && function_exists('gd_info')) { - $temp = gd_info(); - $versions['gd'] = ['title' => Lang::$txt['support_versions_gd'], 'version' => $temp['GD Version']]; - } + $versions['imagemagick'] = ['title' => Lang::$txt['support_versions_imagemagick'], 'version' => $im_version . ' (' . $extension_version . ')']; + } - // Why not have a look at ImageMagick? If it's installed, we should show version information for it too. - if (in_array('imagemagick', $checkFor) && class_exists('Imagick')) { - $temp = new \Imagick(); - $temp2 = $temp->getVersion(); - $im_version = $temp2['versionString']; - $extension_version = 'Imagick ' . phpversion('Imagick'); + // Now lets check for the Database. + if (in_array('db_server', $checkFor)) { + if (!isset(Db::$db_connection) || Db::$db_connection === false) { + Lang::load('Errors'); + trigger_error(Lang::$txt['get_server_versions_no_database'], E_USER_NOTICE); + } else { + $versions['db_engine'] = [ + 'title' => Lang::getTxt('support_versions_db_engine', ['db_title' => Db::$db->title]), + 'version' => Db::$db->get_vendor(), + ]; - // We already know it's ImageMagick and the website isn't needed... - $im_version = str_replace(['ImageMagick ', ' https://www.imagemagick.org'], '', $im_version); + $versions['db_server'] = [ + 'title' => Lang::getTxt('support_versions_db', ['db_title' => Db::$db->title]), + 'version' => Db::$db->get_version(), + ]; + } + } - $versions['imagemagick'] = ['title' => Lang::$txt['support_versions_imagemagick'], 'version' => $im_version . ' (' . $extension_version . ')']; - } + // Check to see if we have any accelerators installed. + foreach (CacheApi::detect() as $class_name => $cache_api) { + $class_name_txt_key = strtolower($cache_api->getImplementationClassKeyName()); - // Now lets check for the Database. - if (in_array('db_server', $checkFor)) { - if (!isset(Db::$db_connection) || Db::$db_connection === false) { - Lang::load('Errors'); - trigger_error(Lang::$txt['get_server_versions_no_database'], E_USER_NOTICE); - } else { - $versions['db_engine'] = [ - 'title' => Lang::getTxt('support_versions_db_engine', ['db_title' => Db::$db->title]), - 'version' => Db::$db->get_vendor(), - ]; + if (in_array($class_name_txt_key, $checkFor)) { + $versions[$class_name_txt_key] = [ + 'title' => Lang::$txt[$class_name_txt_key . '_cache'] ?? $class_name, + 'version' => $cache_api->getVersion(), + ]; + } + } - $versions['db_server'] = [ - 'title' => Lang::getTxt('support_versions_db', ['db_title' => Db::$db->title]), - 'version' => Db::$db->get_version(), + if (in_array('php', $checkFor)) { + $versions['php'] = [ + 'title' => 'PHP', + 'version' => PHP_VERSION, + 'more' => '?action=admin;area=serversettings;sa=phpinfo', ]; } - } - // Check to see if we have any accelerators installed. - foreach (CacheApi::detect() as $class_name => $cache_api) { - $class_name_txt_key = strtolower($cache_api->getImplementationClassKeyName()); - - if (in_array($class_name_txt_key, $checkFor)) { - $versions[$class_name_txt_key] = [ - 'title' => Lang::$txt[$class_name_txt_key . '_cache'] ?? $class_name, - 'version' => $cache_api->getVersion(), + if (in_array('server', $checkFor)) { + $versions['server'] = [ + 'title' => Lang::$txt['support_versions_server'], + 'version' => $_SERVER['SERVER_SOFTWARE'], ]; } + + return $versions; } - if (in_array('php', $checkFor)) { - $versions['php'] = [ - 'title' => 'PHP', - 'version' => PHP_VERSION, - 'more' => '?action=admin;area=serversettings;sa=phpinfo', + /** + * Search through source, theme, and language files to determine their version. + * Get detailed version information about the physical SMF files on the server. + * + * - the input parameter allows to set whether to include SSI.php and whether + * the results should be sorted. + * - returns an array containing information on source files, templates, and + * language files found in the default theme directory (grouped by language). + * + * @param array &$versionOptions An array of options. Can contain one or more of 'include_root', 'include_tasks' and 'sort_results' + * @return array An array of file version info. + */ + public static function getFileVersions(array &$versionOptions): array + { + // Default place to find the languages would be the default theme dir. + $lang_dir = Theme::$current->settings['default_theme_dir'] . '/languages'; + + $version_info = [ + 'root_versions' => [], + 'file_versions' => [], + 'default_template_versions' => [], + 'template_versions' => [], + 'default_language_versions' => [], + 'tasks_versions' => [], ]; - } - if (in_array('server', $checkFor)) { - $versions['server'] = [ - 'title' => Lang::$txt['support_versions_server'], - 'version' => $_SERVER['SERVER_SOFTWARE'], + $root_files = [ + 'cron.php', + 'proxy.php', + 'SSI.php', + 'subscriptions.php', ]; - } - return $versions; - } + // Find the version in root files header. + if (!empty($versionOptions['include_root'])) { + foreach ($root_files as $file) { + if (!file_exists(Config::$boarddir . '/' . $file)) { + continue; + } - /** - * Search through source, theme, and language files to determine their version. - * Get detailed version information about the physical SMF files on the server. - * - * - the input parameter allows to set whether to include SSI.php and whether - * the results should be sorted. - * - returns an array containing information on source files, templates, and - * language files found in the default theme directory (grouped by language). - * - * @param array &$versionOptions An array of options. Can contain one or more of 'include_root', 'include_tasks' and 'sort_results' - * @return array An array of file version info. - */ - public static function getFileVersions(array &$versionOptions): array - { - // Default place to find the languages would be the default theme dir. - $lang_dir = Theme::$current->settings['default_theme_dir'] . '/languages'; - - $version_info = [ - 'root_versions' => [], - 'file_versions' => [], - 'default_template_versions' => [], - 'template_versions' => [], - 'default_language_versions' => [], - 'tasks_versions' => [], - ]; + $fp = fopen(Config::$boarddir . '/' . $file, 'rb'); + $header = fread($fp, 4096); + fclose($fp); - $root_files = [ - 'cron.php', - 'proxy.php', - 'SSI.php', - 'subscriptions.php', - ]; + // The comment looks roughly like... that. + if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1) { + $version_info['root_versions'][$file] = $match[1]; + } + // Not found! This is bad. + else { + $version_info['root_versions'][$file] = '??'; + } + } + } + + // Load all the files in the Sources directory, except some vendor libraries, index place holders and non php files. + $sources_dir = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( + Config::$sourcedir, + \RecursiveDirectoryIterator::SKIP_DOTS, + ), + ); + + $ignore_sources = [ + Config::$sourcedir . '/minify/*', + Config::$sourcedir . '/ReCaptcha/*', + Config::$sourcedir . '/Tasks/*', + ]; - // Find the version in root files header. - if (!empty($versionOptions['include_root'])) { - foreach ($root_files as $file) { - if (!file_exists(Config::$boarddir . '/' . $file)) { + foreach ($sources_dir as $filename => $file) { + if (!$file->isFile() || $file->getFilename() === 'index.php' || $file->getExtension() !== 'php') { continue; } - $fp = fopen(Config::$boarddir . '/' . $file, 'rb'); - $header = fread($fp, 4096); - fclose($fp); + foreach ($ignore_sources as $if) { + if (preg_match('~' . $if . '~i', $filename)) { + continue 2; + } + } + + $shortname = str_replace(Config::$sourcedir . '/', '', $filename); - // The comment looks roughly like... that. + // Read the first 4k from the file.... enough for the header. + $fp = $file->openFile('rb'); + $header = $fp->fread(4096); + $fp = null; + + // Look for the version comment in the file header. if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1) { - $version_info['root_versions'][$file] = $match[1]; + $version_info['file_versions'][$shortname] = $match[1]; } - // Not found! This is bad. + // It wasn't found, but the file was... show a '??'. else { - $version_info['root_versions'][$file] = '??'; + $version_info['file_versions'][$shortname] = '??'; } } - } - - // Load all the files in the Sources directory, except some vendor libraries, index place holders and non php files. - $sources_dir = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator( - Config::$sourcedir, - \RecursiveDirectoryIterator::SKIP_DOTS, - ), - ); - - $ignore_sources = [ - Config::$sourcedir . '/minify/*', - Config::$sourcedir . '/ReCaptcha/*', - Config::$sourcedir . '/Tasks/*', - ]; - - foreach ($sources_dir as $filename => $file) { - if (!$file->isFile() || $file->getFilename() === 'index.php' || $file->getExtension() !== 'php') { - continue; - } - - foreach ($ignore_sources as $if) { - if (preg_match('~' . $if . '~i', $filename)) { - continue 2; + $sources_dir = null; + + // Load all the files in the tasks directory. + if (!empty($versionOptions['include_tasks'])) { + $tasks_dir = dir(Config::$tasksdir); + + while ($entry = $tasks_dir->read()) { + if (str_ends_with($entry, '.php') && !is_dir(Config::$tasksdir . '/' . $entry) && $entry !== 'index.php') { + // Read the first 4k from the file.... enough for the header. + $fp = fopen(Config::$tasksdir . '/' . $entry, 'rb'); + $header = fread($fp, 4096); + fclose($fp); + + // Look for the version comment in the file header. + if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1) { + $version_info['tasks_versions'][$entry] = $match[1]; + } + // It wasn't found, but the file was... show a '??'. + else { + $version_info['tasks_versions'][$entry] = '??'; + } + } } + $tasks_dir->close(); } - $shortname = str_replace(Config::$sourcedir . '/', '', $filename); + // Load all the files in the default template directory - and the current theme if applicable. + $directories = ['default_template_versions' => Theme::$current->settings['default_theme_dir']]; - // Read the first 4k from the file.... enough for the header. - $fp = $file->openFile('rb'); - $header = $fp->fread(4096); - $fp = null; - - // Look for the version comment in the file header. - if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1) { - $version_info['file_versions'][$shortname] = $match[1]; - } - // It wasn't found, but the file was... show a '??'. - else { - $version_info['file_versions'][$shortname] = '??'; + if (Theme::$current->settings['theme_id'] != 1) { + $directories += ['template_versions' => Theme::$current->settings['theme_dir']]; } - } - $sources_dir = null; - // Load all the files in the tasks directory. - if (!empty($versionOptions['include_tasks'])) { - $tasks_dir = dir(Config::$tasksdir); + foreach ($directories as $type => $dirname) { + $this_dir = dir($dirname); - while ($entry = $tasks_dir->read()) { - if (str_ends_with($entry, '.php') && !is_dir(Config::$tasksdir . '/' . $entry) && $entry !== 'index.php') { - // Read the first 4k from the file.... enough for the header. - $fp = fopen(Config::$tasksdir . '/' . $entry, 'rb'); - $header = fread($fp, 4096); - fclose($fp); + while ($entry = $this_dir->read()) { + if (str_ends_with($entry, 'template.php') && !is_dir($dirname . '/' . $entry)) { + // Read the first 768 bytes from the file.... enough for the header. + $fp = fopen($dirname . '/' . $entry, 'rb'); + $header = fread($fp, 768); + fclose($fp); - // Look for the version comment in the file header. - if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1) { - $version_info['tasks_versions'][$entry] = $match[1]; - } - // It wasn't found, but the file was... show a '??'. - else { - $version_info['tasks_versions'][$entry] = '??'; + // Look for the version comment in the file header. + if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1) { + $version_info[$type][$entry] = $match[1]; + } + // It wasn't found, but the file was... show a '??'. + else { + $version_info[$type][$entry] = '??'; + } } } + $this_dir->close(); } - $tasks_dir->close(); - } - - // Load all the files in the default template directory - and the current theme if applicable. - $directories = ['default_template_versions' => Theme::$current->settings['default_theme_dir']]; - - if (Theme::$current->settings['theme_id'] != 1) { - $directories += ['template_versions' => Theme::$current->settings['theme_dir']]; - } - foreach ($directories as $type => $dirname) { - $this_dir = dir($dirname); + // Load up all the files in the default language directory and sort by language. + $this_dir = dir($lang_dir); while ($entry = $this_dir->read()) { - if (str_ends_with($entry, 'template.php') && !is_dir($dirname . '/' . $entry)) { + if (str_ends_with($entry, '.php') && $entry != 'index.php' && !is_dir($lang_dir . '/' . $entry)) { // Read the first 768 bytes from the file.... enough for the header. - $fp = fopen($dirname . '/' . $entry, 'rb'); + $fp = fopen($lang_dir . '/' . $entry, 'rb'); $header = fread($fp, 768); fclose($fp); + // Split the file name off into useful bits. + list($name, $language) = explode('.', $entry); + // Look for the version comment in the file header. - if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1) { - $version_info[$type][$entry] = $match[1]; + if (preg_match('~(?://|/\*)\s*Version:\s+(.+?);\s*' . preg_quote($name, '~') . '(?:[\s]{2}|\*/)~i', $header, $match) == 1) { + $version_info['default_language_versions'][$language][$name] = $match[1]; } // It wasn't found, but the file was... show a '??'. else { - $version_info[$type][$entry] = '??'; + $version_info['default_language_versions'][$language][$name] = '??'; } } } - $this_dir->close(); - } - // Load up all the files in the default language directory and sort by language. - $this_dir = dir($lang_dir); - - while ($entry = $this_dir->read()) { - if (str_ends_with($entry, '.php') && $entry != 'index.php' && !is_dir($lang_dir . '/' . $entry)) { - // Read the first 768 bytes from the file.... enough for the header. - $fp = fopen($lang_dir . '/' . $entry, 'rb'); - $header = fread($fp, 768); - fclose($fp); - - // Split the file name off into useful bits. - list($name, $language) = explode('.', $entry); + $this_dir->close(); - // Look for the version comment in the file header. - if (preg_match('~(?://|/\*)\s*Version:\s+(.+?);\s*' . preg_quote($name, '~') . '(?:[\s]{2}|\*/)~i', $header, $match) == 1) { - $version_info['default_language_versions'][$language][$name] = $match[1]; - } - // It wasn't found, but the file was... show a '??'. - else { - $version_info['default_language_versions'][$language][$name] = '??'; + // Sort the file versions by filename. + if (!empty($versionOptions['sort_results'])) { + ksort($version_info['file_versions']); + ksort($version_info['default_template_versions']); + ksort($version_info['template_versions']); + ksort($version_info['default_language_versions']); + ksort($version_info['tasks_versions']); + + // For languages sort each language too. + foreach ($version_info['default_language_versions'] as $language => $dummy) { + ksort($version_info['default_language_versions'][$language]); } } - } - $this_dir->close(); - - // Sort the file versions by filename. - if (!empty($versionOptions['sort_results'])) { - ksort($version_info['file_versions']); - ksort($version_info['default_template_versions']); - ksort($version_info['template_versions']); - ksort($version_info['default_language_versions']); - ksort($version_info['tasks_versions']); - - // For languages sort each language too. - foreach ($version_info['default_language_versions'] as $language => $dummy) { - ksort($version_info['default_language_versions'][$language]); - } + return $version_info; } - return $version_info; - } + /** + * Saves the admin's current preferences to the database. + */ + public static function updateAdminPreferences(): void + { + // This must exist! + if (!isset(Utils::$context['admin_preferences'])) { + return; + } - /** - * Saves the admin's current preferences to the database. - */ - public static function updateAdminPreferences(): void - { - // This must exist! - if (!isset(Utils::$context['admin_preferences'])) { - return; + // This is what we'll be saving. + Theme::$current->options['admin_preferences'] = Utils::jsonEncode(Utils::$context['admin_preferences']); + + // Just check we haven't ended up with something theme exclusive somehow. + Db::$db->query( + '', + 'DELETE FROM {db_prefix}themes + WHERE id_theme != {int:default_theme} + AND variable = {string:admin_preferences}', + [ + 'default_theme' => 1, + 'admin_preferences' => 'admin_preferences', + ], + ); + + // Update the themes table. + Db::$db->insert( + 'replace', + '{db_prefix}themes', + ['id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'], + [User::$me->id, 1, 'admin_preferences', Theme::$current->options['admin_preferences']], + ['id_member', 'id_theme', 'variable'], + ); + + // Make sure we invalidate any cache. + CacheApi::put('theme_settings-' . Theme::$current->settings['theme_id'] . ':' . User::$me->id, null, 0); } - // This is what we'll be saving. - Theme::$current->options['admin_preferences'] = Utils::jsonEncode(Utils::$context['admin_preferences']); - - // Just check we haven't ended up with something theme exclusive somehow. - Db::$db->query( - '', - 'DELETE FROM {db_prefix}themes - WHERE id_theme != {int:default_theme} - AND variable = {string:admin_preferences}', - [ - 'default_theme' => 1, - 'admin_preferences' => 'admin_preferences', - ], - ); - - // Update the themes table. - Db::$db->insert( - 'replace', - '{db_prefix}themes', - ['id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'], - [User::$me->id, 1, 'admin_preferences', Theme::$current->options['admin_preferences']], - ['id_member', 'id_theme', 'variable'], - ); - - // Make sure we invalidate any cache. - CacheApi::put('theme_settings-' . Theme::$current->settings['theme_id'] . ':' . User::$me->id, null, 0); - } + /** + * Send all the administrators a lovely email. + * - loads all users who are admins or have the admin forum permission. + * - uses the email template and replacements passed in the parameters. + * - sends them an email. + * + * @param string $template Which email template to use + * @param array $replacements An array of items to replace the variables in the template + * @param array $additional_recipients An array of arrays of info for additional recipients. Should have 'id', 'email' and 'name' for each. + */ + public static function emailAdmins(string $template, array $replacements = [], array $additional_recipients = []): void + { + // Load all members which are effectively admins. + $members = User::membersAllowedTo('admin_forum'); + + // Load their alert preferences + $prefs = Notify::getNotifyPrefs($members, 'announcements', true); + + $emails_sent = []; + + $request = Db::$db->query( + '', + 'SELECT id_member, member_name, real_name, lngfile, email_address + FROM {db_prefix}members + WHERE id_member IN({array_int:members})', + [ + 'members' => $members, + ], + ); - /** - * Send all the administrators a lovely email. - * - loads all users who are admins or have the admin forum permission. - * - uses the email template and replacements passed in the parameters. - * - sends them an email. - * - * @param string $template Which email template to use - * @param array $replacements An array of items to replace the variables in the template - * @param array $additional_recipients An array of arrays of info for additional recipients. Should have 'id', 'email' and 'name' for each. - */ - public static function emailAdmins(string $template, array $replacements = [], array $additional_recipients = []): void - { - // Load all members which are effectively admins. - $members = User::membersAllowedTo('admin_forum'); + while ($row = Db::$db->fetch_assoc($request)) { + if (empty($prefs[$row['id_member']]['announcements'])) { + continue; + } - // Load their alert preferences - $prefs = Notify::getNotifyPrefs($members, 'announcements', true); + // Stick their particulars in the replacement data. + $replacements['IDMEMBER'] = $row['id_member']; + $replacements['REALNAME'] = $row['member_name']; + $replacements['USERNAME'] = $row['real_name']; - $emails_sent = []; + // Load the data from the template. + $emaildata = Mail::loadEmailTemplate($template, $replacements, empty($row['lngfile']) || empty(Config::$modSettings['userLanguage']) ? Lang::$default : $row['lngfile']); - $request = Db::$db->query( - '', - 'SELECT id_member, member_name, real_name, lngfile, email_address - FROM {db_prefix}members - WHERE id_member IN({array_int:members})', - [ - 'members' => $members, - ], - ); + // Then send the actual email. + Mail::send($row['email_address'], $emaildata['subject'], $emaildata['body'], null, $template, $emaildata['is_html'], 1); - while ($row = Db::$db->fetch_assoc($request)) { - if (empty($prefs[$row['id_member']]['announcements'])) { - continue; + // Track who we emailed so we don't do it twice. + $emails_sent[] = $row['email_address']; } + Db::$db->free_result($request); - // Stick their particulars in the replacement data. - $replacements['IDMEMBER'] = $row['id_member']; - $replacements['REALNAME'] = $row['member_name']; - $replacements['USERNAME'] = $row['real_name']; + // Any additional users we must email this to? + if (!empty($additional_recipients)) { + foreach ($additional_recipients as $recipient) { + if (in_array($recipient['email'], $emails_sent)) { + continue; + } - // Load the data from the template. - $emaildata = Mail::loadEmailTemplate($template, $replacements, empty($row['lngfile']) || empty(Config::$modSettings['userLanguage']) ? Lang::$default : $row['lngfile']); + $replacements['IDMEMBER'] = $recipient['id']; + $replacements['REALNAME'] = $recipient['name']; + $replacements['USERNAME'] = $recipient['name']; - // Then send the actual email. - Mail::send($row['email_address'], $emaildata['subject'], $emaildata['body'], null, $template, $emaildata['is_html'], 1); + // Load the template again. + $emaildata = Mail::loadEmailTemplate($template, $replacements, empty($recipient['lang']) || empty(Config::$modSettings['userLanguage']) ? Lang::$default : $recipient['lang']); - // Track who we emailed so we don't do it twice. - $emails_sent[] = $row['email_address']; + // Send off the email. + Mail::send($recipient['email'], $emaildata['subject'], $emaildata['body'], null, $template, $emaildata['is_html'], 1); + } + } } - Db::$db->free_result($request); - // Any additional users we must email this to? - if (!empty($additional_recipients)) { - foreach ($additional_recipients as $recipient) { - if (in_array($recipient['email'], $emails_sent)) { - continue; + /** + * Question the verity of the admin by asking for his or her password. + * - loads Login.template.php and uses the admin_login sub template. + * - sends data to template so the admin is sent on to the page they + * wanted if their password is correct, otherwise they can try again. + * + * @param string $type What login type is this - can be 'admin' or 'moderate' + */ + public static function adminLogin(string $type = 'admin'): void + { + Lang::load('Admin'); + Theme::loadTemplate('Login'); + + // Validate what type of session check this is. + $types = []; + IntegrationHook::call('integrate_validateSession', [&$types]); + $type = in_array($type, $types) || $type == 'moderate' ? $type : 'admin'; + + // They used a wrong password, log it and unset that. + if (isset($_POST[$type . '_hash_pass']) || isset($_POST[$type . '_pass'])) { + Lang::$txt['security_wrong'] = Lang::getTxt('security_wrong', ['referrer' => $_SERVER['HTTP_REFERER'] ?? Lang::$txt['unknown'], 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'ip' => User::$me->ip]); + ErrorHandler::log(Lang::$txt['security_wrong'], 'critical'); + + if (isset($_POST[$type . '_hash_pass'])) { + unset($_POST[$type . '_hash_pass']); } - $replacements['IDMEMBER'] = $recipient['id']; - $replacements['REALNAME'] = $recipient['name']; - $replacements['USERNAME'] = $recipient['name']; - - // Load the template again. - $emaildata = Mail::loadEmailTemplate($template, $replacements, empty($recipient['lang']) || empty(Config::$modSettings['userLanguage']) ? Lang::$default : $recipient['lang']); + if (isset($_POST[$type . '_pass'])) { + unset($_POST[$type . '_pass']); + } - // Send off the email. - Mail::send($recipient['email'], $emaildata['subject'], $emaildata['body'], null, $template, $emaildata['is_html'], 1); + Utils::$context['incorrect_password'] = true; } - } - } - - /** - * Question the verity of the admin by asking for his or her password. - * - loads Login.template.php and uses the admin_login sub template. - * - sends data to template so the admin is sent on to the page they - * wanted if their password is correct, otherwise they can try again. - * - * @param string $type What login type is this - can be 'admin' or 'moderate' - */ - public static function adminLogin(string $type = 'admin'): void - { - Lang::load('Admin'); - Theme::loadTemplate('Login'); - // Validate what type of session check this is. - $types = []; - IntegrationHook::call('integrate_validateSession', [&$types]); - $type = in_array($type, $types) || $type == 'moderate' ? $type : 'admin'; + SecurityToken::create('admin-login'); - // They used a wrong password, log it and unset that. - if (isset($_POST[$type . '_hash_pass']) || isset($_POST[$type . '_pass'])) { - Lang::$txt['security_wrong'] = Lang::getTxt('security_wrong', ['referrer' => $_SERVER['HTTP_REFERER'] ?? Lang::$txt['unknown'], 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'ip' => User::$me->ip]); - ErrorHandler::log(Lang::$txt['security_wrong'], 'critical'); + // Figure out the get data and post data. + Utils::$context['get_data'] = '?' . self::construct_query_string($_GET); + Utils::$context['post_data'] = ''; - if (isset($_POST[$type . '_hash_pass'])) { - unset($_POST[$type . '_hash_pass']); - } + // Now go through $_POST. Make sure the session hash is sent. + $_POST[Utils::$context['session_var']] = Utils::$context['session_id']; - if (isset($_POST[$type . '_pass'])) { - unset($_POST[$type . '_pass']); + foreach ($_POST as $k => $v) { + Utils::$context['post_data'] .= self::adminLogin_outputPostVars($k, $v); } - Utils::$context['incorrect_password'] = true; - } - - SecurityToken::create('admin-login'); - - // Figure out the get data and post data. - Utils::$context['get_data'] = '?' . self::construct_query_string($_GET); - Utils::$context['post_data'] = ''; + // Now we'll use the admin_login sub template of the Login template. + Utils::$context['sub_template'] = 'admin_login'; - // Now go through $_POST. Make sure the session hash is sent. - $_POST[Utils::$context['session_var']] = Utils::$context['session_id']; + // And title the page something like "Login". + if (!isset(Utils::$context['page_title'])) { + Utils::$context['page_title'] = Lang::$txt['login']; + } - foreach ($_POST as $k => $v) { - Utils::$context['post_data'] .= self::adminLogin_outputPostVars($k, $v); - } + // The type of action. + Utils::$context['sessionCheckType'] = $type; - // Now we'll use the admin_login sub template of the Login template. - Utils::$context['sub_template'] = 'admin_login'; + Utils::obExit(); - // And title the page something like "Login". - if (!isset(Utils::$context['page_title'])) { - Utils::$context['page_title'] = Lang::$txt['login']; + // We MUST exit at this point, because otherwise we CANNOT KNOW that the user is privileged. + trigger_error('No direct access...', E_USER_ERROR); } - // The type of action. - Utils::$context['sessionCheckType'] = $type; - - Utils::obExit(); - - // We MUST exit at this point, because otherwise we CANNOT KNOW that the user is privileged. - trigger_error('No direct access...', E_USER_ERROR); - } - - /****************** - * Internal methods - ******************/ + /****************** + * Internal methods + ******************/ + + /** + * Constructor. Protected to force instantiation via self::load(). + */ + protected function __construct() + { + // Load the language and templates.... + Lang::load('Admin'); + Theme::loadTemplate('Admin'); + Theme::loadJavaScriptFile('admin.js', ['minimize' => true], 'smf_admin'); + Theme::loadCSSFile('admin.css', [], 'smf_admin'); + + // Set any dynamic values in $this->admin_areas. + $this->setAdminAreas(); + + // No indexing evil stuff. + Utils::$context['robot_no_index'] = true; + + // Some preferences. + Utils::$context['admin_preferences'] = !empty(Theme::$current->options['admin_preferences']) ? Utils::jsonDecode(Theme::$current->options['admin_preferences'], true) : []; + + // Any files to include for administration? + if (!empty(Config::$modSettings['integrate_admin_include'])) { + $admin_includes = explode(',', Config::$modSettings['integrate_admin_include']); + + foreach ($admin_includes as $include) { + $include = strtr(trim($include), [ + '$boarddir' => Config::$boarddir, + '$sourcedir' => Config::$sourcedir, + '$themedir' => Theme::$current->settings['theme_dir'], + ]); - /** - * Constructor. Protected to force instantiation via self::load(). - */ - protected function __construct() - { - // Load the language and templates.... - Lang::load('Admin'); - Theme::loadTemplate('Admin'); - Theme::loadJavaScriptFile('admin.js', ['minimize' => true], 'smf_admin'); - Theme::loadCSSFile('admin.css', [], 'smf_admin'); - - // Set any dynamic values in $this->admin_areas. - $this->setAdminAreas(); - - // No indexing evil stuff. - Utils::$context['robot_no_index'] = true; - - // Some preferences. - Utils::$context['admin_preferences'] = !empty(Theme::$current->options['admin_preferences']) ? Utils::jsonDecode(Theme::$current->options['admin_preferences'], true) : []; - - // Any files to include for administration? - if (!empty(Config::$modSettings['integrate_admin_include'])) { - $admin_includes = explode(',', Config::$modSettings['integrate_admin_include']); - - foreach ($admin_includes as $include) { - $include = strtr(trim($include), [ - '$boarddir' => Config::$boarddir, - '$sourcedir' => Config::$sourcedir, - '$themedir' => Theme::$current->settings['theme_dir'], - ]); - - if (file_exists($include)) { - require_once $include; + if (file_exists($include)) { + require_once $include; + } } } } - } - - /** - * Sets any dynamic values in $this->admin_areas. - */ - protected function setAdminAreas(): void - { - // Finalize various string values. - array_walk_recursive( - $this->admin_areas, - function (&$value, $key) { - if (in_array($key, ['title', 'label'])) { - $value = Lang::$txt[$value] ?? $value; - } - - if (is_string($value)) { - $value = strtr($value, [ - '{scripturl}' => Config::$scripturl, - '{boardurl}' => Config::$boardurl, - ]); - } - }, - ); - // Fill in the ID number for the current theme URL. - $this->admin_areas['config']['areas']['current_theme']['custom_url'] = sprintf($this->admin_areas['config']['areas']['current_theme']['custom_url'], Theme::$current->settings['theme_id']); - - // Figure out what is enabled or not. - $this->admin_areas['forum']['areas']['adminlogoff']['enabled'] = empty(Config::$modSettings['securityDisable']); + /** + * Sets any dynamic values in $this->admin_areas. + */ + protected function setAdminAreas(): void + { + // Finalize various string values. + array_walk_recursive( + $this->admin_areas, + function (&$value, $key) { + if (in_array($key, ['title', 'label'])) { + $value = Lang::$txt[$value] ?? $value; + } - if (empty(Config::$modSettings['cal_enabled'])) { - $this->admin_areas['layout']['areas']['managecalendar']['inactive'] = true; - $this->admin_areas['layout']['areas']['managecalendar']['subsections'] = []; - } + if (is_string($value)) { + $value = strtr($value, [ + '{scripturl}' => Config::$scripturl, + '{boardurl}' => Config::$boardurl, + ]); + } + }, + ); - $this->admin_areas['layout']['areas']['smileys']['subsections']['addsmiley']['enabled'] = !empty(Config::$modSettings['smiley_enable']); - $this->admin_areas['layout']['areas']['smileys']['subsections']['editsmileys']['enabled'] = !empty(Config::$modSettings['smiley_enable']); - $this->admin_areas['layout']['areas']['smileys']['subsections']['setorder']['enabled'] = !empty(Config::$modSettings['smiley_enable']); - $this->admin_areas['layout']['areas']['smileys']['subsections']['editicons']['enabled'] = !empty(Config::$modSettings['messageIcons_enable']); + // Fill in the ID number for the current theme URL. + $this->admin_areas['config']['areas']['current_theme']['custom_url'] = sprintf($this->admin_areas['config']['areas']['current_theme']['custom_url'], Theme::$current->settings['theme_id']); - if (empty(Config::$modSettings['spider_mode'])) { - $this->admin_areas['layout']['areas']['sengines']['inactive'] = true; - $this->admin_areas['layout']['areas']['sengines']['subsections'] = []; - } + // Figure out what is enabled or not. + $this->admin_areas['forum']['areas']['adminlogoff']['enabled'] = empty(Config::$modSettings['securityDisable']); - $this->admin_areas['members']['areas']['warnings']['inactive'] = Config::$modSettings['warning_settings'][0] == 0; + if (empty(Config::$modSettings['cal_enabled'])) { + $this->admin_areas['layout']['areas']['managecalendar']['inactive'] = true; + $this->admin_areas['layout']['areas']['managecalendar']['subsections'] = []; + } - if (empty(Config::$modSettings['paid_enabled'])) { - $this->admin_areas['members']['areas']['paidsubscribe']['inactive'] = true; - $this->admin_areas['members']['areas']['paidsubscribe']['subsections'] = []; - } + $this->admin_areas['layout']['areas']['smileys']['subsections']['addsmiley']['enabled'] = !empty(Config::$modSettings['smiley_enable']); + $this->admin_areas['layout']['areas']['smileys']['subsections']['editsmileys']['enabled'] = !empty(Config::$modSettings['smiley_enable']); + $this->admin_areas['layout']['areas']['smileys']['subsections']['setorder']['enabled'] = !empty(Config::$modSettings['smiley_enable']); + $this->admin_areas['layout']['areas']['smileys']['subsections']['editicons']['enabled'] = !empty(Config::$modSettings['messageIcons_enable']); - $this->admin_areas['maintenance']['areas']['logs']['subsections']['errorlog']['enabled'] = !empty(Config::$modSettings['enableErrorLogging']); - $this->admin_areas['maintenance']['areas']['logs']['subsections']['adminlog']['enabled'] = !empty(Config::$modSettings['adminlog_enabled']); - $this->admin_areas['maintenance']['areas']['logs']['subsections']['modlog']['enabled'] = !empty(Config::$modSettings['modlog_enabled']); - $this->admin_areas['maintenance']['areas']['logs']['subsections']['spiderlog']['enabled'] = !empty(Config::$modSettings['spider_mode']); + $this->admin_areas['layout']['areas']['managereactions']['subsections']['edit']['enabled'] = !empty(Config::$modSettings['enable_reacts']); - // Give mods access to the menu. - IntegrationHook::call('integrate_admin_areas', [&$this->admin_areas]); - } + if (empty(Config::$modSettings['spider_mode'])) { + $this->admin_areas['layout']['areas']['sengines']['inactive'] = true; + $this->admin_areas['layout']['areas']['sengines']['subsections'] = []; + } - /************************* - * Internal static methods - *************************/ + $this->admin_areas['members']['areas']['warnings']['inactive'] = Config::$modSettings['warning_settings'][0] == 0; - /** - * Used by the adminLogin() method. - * - * If 'value' is an array, calls itself recursively. - * - * @param string $k The keys - * @param string|array $v The values - * @return string 'hidden' HTML form fields, containing key-value pairs - */ - protected static function adminLogin_outputPostVars(string $k, string|array $v): string - { - if (!is_array($v)) { - return "\n" . ' '"', '<' => '<', '>' => '>']) . '">'; - } + if (empty(Config::$modSettings['paid_enabled'])) { + $this->admin_areas['members']['areas']['paidsubscribe']['inactive'] = true; + $this->admin_areas['members']['areas']['paidsubscribe']['subsections'] = []; + } - $ret = ''; + $this->admin_areas['maintenance']['areas']['logs']['subsections']['errorlog']['enabled'] = !empty(Config::$modSettings['enableErrorLogging']); + $this->admin_areas['maintenance']['areas']['logs']['subsections']['adminlog']['enabled'] = !empty(Config::$modSettings['adminlog_enabled']); + $this->admin_areas['maintenance']['areas']['logs']['subsections']['modlog']['enabled'] = !empty(Config::$modSettings['modlog_enabled']); + $this->admin_areas['maintenance']['areas']['logs']['subsections']['spiderlog']['enabled'] = !empty(Config::$modSettings['spider_mode']); - foreach ($v as $k2 => $v2) { - $ret .= self::adminLogin_outputPostVars($k . '[' . $k2 . ']', $v2); + // Give mods access to the menu. + IntegrationHook::call('integrate_admin_areas', [&$this->admin_areas]); } - return $ret; - } + /************************* + * Internal static methods + *************************/ + + /** + * Used by the adminLogin() method. + * + * If 'value' is an array, calls itself recursively. + * + * @param string $k The keys + * @param string|array $v The values + * @return string 'hidden' HTML form fields, containing key-value pairs + */ + protected static function adminLogin_outputPostVars(string $k, string|array $v): string + { + if (!is_array($v)) { + return "\n" . ' '"', '<' => '<', '>' => '>']) . '">'; + } - /** - * Properly urlencodes a string to be used in a query. - * - * @param array $get A copy of $_GET. - * @return string Our query string. - */ - protected static function construct_query_string(array $get): string - { - $query_string = ''; + $ret = ''; - // Awww, darn. The Config::$scripturl contains GET stuff! - $q = strpos(Config::$scripturl, '?'); + foreach ($v as $k2 => $v2) { + $ret .= self::adminLogin_outputPostVars($k . '[' . $k2 . ']', $v2); + } - if ($q !== false) { - parse_str(preg_replace('/&(\w+)(?=&|$)/', '&$1=', strtr(substr(Config::$scripturl, $q + 1), ';', '&')), $temp); + return $ret; + } - foreach ($get as $k => $v) { - // Only if it's not already in the Config::$scripturl! - if (!isset($temp[$k])) { - $query_string .= urlencode($k) . '=' . urlencode($v) . ';'; + /** + * Properly urlencodes a string to be used in a query. + * + * @param array $get A copy of $_GET. + * @return string Our query string. + */ + protected static function construct_query_string(array $get): string + { + $query_string = ''; + + // Awww, darn. The Config::$scripturl contains GET stuff! + $q = strpos(Config::$scripturl, '?'); + + if ($q !== false) { + parse_str(preg_replace('/&(\w+)(?=&|$)/', '&$1=', strtr(substr(Config::$scripturl, $q + 1), ';', '&')), $temp); + + foreach ($get as $k => $v) { + // Only if it's not already in the Config::$scripturl! + if (!isset($temp[$k])) { + $query_string .= urlencode($k) . '=' . urlencode($v) . ';'; + } + // If it changed, put it out there, but with an ampersand. + elseif ($temp[$k] != $get[$k]) { + $query_string .= urlencode($k) . '=' . urlencode($v) . '&'; + } } - // If it changed, put it out there, but with an ampersand. - elseif ($temp[$k] != $get[$k]) { - $query_string .= urlencode($k) . '=' . urlencode($v) . '&'; + } else { + // Add up all the data from $_GET into get_data. + foreach ($get as $k => $v) { + $query_string .= urlencode($k) . '=' . urlencode($v) . ';'; } } - } else { - // Add up all the data from $_GET into get_data. - foreach ($get as $k => $v) { - $query_string .= urlencode($k) . '=' . urlencode($v) . ';'; - } - } - $query_string = substr($query_string, 0, -1); + $query_string = substr($query_string, 0, -1); - return $query_string; + return $query_string; + } } -} -?> \ No newline at end of file + ?> \ No newline at end of file diff --git a/Sources/Actions/Admin/Features.php b/Sources/Actions/Admin/Features.php index 4c6329c0c0..b39a73f0f2 100644 --- a/Sources/Actions/Admin/Features.php +++ b/Sources/Actions/Admin/Features.php @@ -73,7 +73,6 @@ class Features implements ActionInterface 'sig' => 'signature', 'profile' => 'profile', 'profileedit' => 'profileEdit', - 'likes' => 'likes', 'mentions' => 'mentions', 'alerts' => 'alerts', ]; @@ -1415,32 +1414,6 @@ public function profileEdit(): void SecurityToken::create('admin-ecp'); } - /** - * Handles modifying the likes settings. - * - * Accessed from ?action=admin;area=featuresettings;sa=likes - */ - public function likes(): void - { - $config_vars = self::likesConfigVars(); - - // Saving? - if (isset($_GET['save'])) { - User::$me->checkSession(); - - IntegrationHook::call('integrate_save_likes_settings'); - - ACP::saveDBSettings($config_vars); - $_SESSION['adm-save'] = true; - Utils::redirectexit('action=admin;area=featuresettings;sa=likes'); - } - - Utils::$context['post_url'] = Config::$scripturl . '?action=admin;area=featuresettings;save;sa=likes'; - Utils::$context['settings_title'] = Lang::$txt['likes']; - - ACP::prepareDBSettingContext($config_vars); - } - /** * Handles modifying the mentions settings. * @@ -1700,23 +1673,6 @@ public static function sigConfigVars(): array return $config_vars; } - /** - * Gets the configuration variables for the likes sub-action. - * - * @return array $config_vars for the likes sub-action. - */ - public static function likesConfigVars(): array - { - $config_vars = [ - ['check', 'enable_likes'], - ['permissions', 'likes_like'], - ]; - - IntegrationHook::call('integrate_likes_settings', [&$config_vars]); - - return $config_vars; - } - /** * Gets the configuration variables for the mentions sub-action. * @@ -1889,14 +1845,14 @@ public static function modifySignatureSettings(bool $return_config = false): ?ar * @param bool $return_config Whether to return the config_vars array. * @return ?array Returns nothing or returns the config_vars array. */ - public static function modifyLikesSettings($return_config = false): ?array + public static function modifyReactionsSettings($return_config = false): ?array { if (!empty($return_config)) { - return self::likesConfigVars(); + return self::reactionsConfigVars(); } self::load(); - self::$obj->subaction = 'likes'; + self::$obj->subaction = 'reactions'; self::$obj->execute(); return null; diff --git a/Sources/Actions/Admin/Find.php b/Sources/Actions/Admin/Find.php index 82dc01b803..c62e62abdd 100644 --- a/Sources/Actions/Admin/Find.php +++ b/Sources/Actions/Admin/Find.php @@ -89,6 +89,7 @@ class Find implements ActionInterface [__NAMESPACE__ . '\\Server::exportConfigVars', 'area=serversettings;sa=export'], [__NAMESPACE__ . '\\Server::loadBalancingConfigVars', 'area=serversettings;sa=loads'], [__NAMESPACE__ . '\\Languages::getConfigVars', 'area=languages;sa=settings'], + [__NAMESPACE__ . '\\Reactions::getConfigVars', 'area=reactions;sa=settings'], [__NAMESPACE__ . '\\Registration::getConfigVars', 'area=regcenter;sa=settings'], [__NAMESPACE__ . '\\SearchEngines::getConfigVars', 'area=sengines;sa=settings'], [__NAMESPACE__ . '\\Subscriptions::getConfigVars', 'area=paidsubscribe;sa=settings'], diff --git a/Sources/Actions/Admin/Permissions.php b/Sources/Actions/Admin/Permissions.php index 7a7be76d17..1edef07c66 100644 --- a/Sources/Actions/Admin/Permissions.php +++ b/Sources/Actions/Admin/Permissions.php @@ -129,7 +129,7 @@ class Permissions implements ActionInterface 'member_admin', 'profile', 'profile_account', - 'likes', + 'reactions', 'mentions', 'bbc', ], @@ -341,8 +341,8 @@ class Permissions implements ActionInterface 'group_level' => self::GROUP_LEVEL_MODERATOR, 'never_guests' => true, ], - 'likes_like' => [ - 'view_group' => 'likes', + 'reactions_react' => [ + 'view_group' => 'reactions', 'scope' => 'global', 'group_level' => self::GROUP_LEVEL_STANDARD, 'never_guests' => true, @@ -1675,9 +1675,9 @@ public static function getPermissions(): array self::$permissions['post_attachment']['hidden'] = true; } - // If likes are disabled, disable the related permission. - if (empty(Config::$modSettings['enable_likes'])) { - self::$permissions['likes_like']['hidden'] = true; + // If reactions are disabled, disable the related permission. + if (empty(Config::$modSettings['enable_reacts'])) { + self::$permissions['reactions_react']['hidden'] = true; } // If mentions are disabled, disable the related permission. diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php new file mode 100644 index 0000000000..ddb06ac7ba --- /dev/null +++ b/Sources/Actions/Admin/Reactions.php @@ -0,0 +1,384 @@ + 'editreactions', + 'settings' => 'settings', + ]; + + /*********************** + * Public methods + ***********************/ + + /** + * Handles modifying reactions settings + */ + public static function settings(): void + { + $config_vars = self::getConfigVars(); + + // Setup the basics of the settings template. + Utils::$context['sub_template'] = 'show_settings'; + Utils::$context['page_title'] = Lang::$txt['reactions_settings']; + + if (isset($_REQUEST['save'])) { + User::$me->checkSession(); + SecurityToken::validate('admin-mr'); + IntegrationHook::call('integrate_save_reactions_settings'); + + // Yeppers, saving this... + ACP::saveDBSettings($config_vars); + + $_SESSION['adm-save'] = true; + Utils::redirectexit('action=admin;area=managereactions;sa=settings'); + } + + // Finish up the form... + Utils::$context['post_url'] = Config::$scripturl . '?action=admin;area=managereactions;save;sa=settings'; + Utils::$context['settings_title'] = Lang::$txt['reactions_settings']; + + // We need this for the in-line permissions + SecurityToken::create('admin-mr'); + + ACP::prepareDBSettingContext($config_vars); + } + + /*********************** + * Public static methods + ***********************/ + + /** + * Gets the configuration variables for the settings sub-action. + * + * @return array $config_vars for the settings sub-action. + */ + public static function getConfigVars(): array + { + $config_vars = [ + ['check', 'enable_reacts'], + ['permissions', 'reactions_react'], + ]; + + IntegrationHook::call('integrate_reactions_settings', [&$config_vars]); + + return $config_vars; + } + + /** + * Dispatcher to whichever sub-action method is necessary. + */ + public function execute(): void + { + $call = method_exists($this, self::$subactions[$this->subaction]) ? [$this, self::$subactions[$this->subaction]] : Utils::getCallable(self::$subactions[$this->subaction]); + + if (!empty($call)) { + call_user_func($call); + } + } + + /** + * Handle adding, deleting and editing reactions + */ + public function editreactions(): void + { + // Make sure we select the right menu item + Menu::$loaded['admin']['currentsubsection'] = 'editreactions'; + + // Get the reactions. If we're updating things then we'll overwrite this later + $reactions = $this->getReactions(); + + // They must have submitted a form. + if (isset($_POST['reacts_save']) || isset($_POST['reacts_delete'])) { + User::$me->checkSession(); + SecurityToken::validate('admin-mre'); + + // This will indicate whether we need to update the reactions cache later... + $do_update = false; + + // Anything to delete? + if (isset($_POST['reacts_delete']) && isset($_POST['delete_reacts'])) { + $do_update = true; + $deleted = []; + + foreach ($_POST['delete_reacts'] as $to_delete) { + $deleted[] = (int) $to_delete; + } + + // Now to do the actual deleting + Db::$db->query('', ' + DELETE FROM {db_prefix}reactions + WHERE id_reaction IN ({array_int:deleted})', + [ + 'deleted' => $deleted, + ] + ); + + // Are there any posts that used these reactions? + $get_reacted_posts = Db::$db->query('', ' + SELECT id_msg, COUNT (id_react) AS num_reacts + FROM {db_prefix}reactions + WHERE id_reaction IN ({array_int:deleted}) + GROUP BY id_msg', + [ + 'deleted' => $deleted, + ] + ); + + // Update the number of reactions for the affected post(s) + // Did we find anything? + if (Db::$db->num_rows($get_reacted_posts) > 0) { + while ($reacted_post = $get_reacted_posts->fetchAssoc()) { + Db::$db->query('', ' + UPDATE {db_prefix}messages + SET reactions = reactions-{int:deleted} + WHERE id_msg = {int:msg}', + [ + 'deleted' => $reacted_post['num_reacts'], + 'msg' => $reacted_post['id_msg'], + ] + ); + } + } + } // Updating things? + elseif (isset($_POST['reacts'])) { + // Adding things? + if (isset($_POST['reacts_add'])) { + foreach ($_POST['reacts_add'] as $new_react) { + // No funny stuff now.. + $new_react = trim($new_react); + if (!empty($new_react)) { + $add[] = [$new_react]; + } + } + + if (!empty($add)) { + $do_update = true; + + // Insert the new reactions + Db::$db->insert('', '{db_prefix}reactions', ['name' => 'string'], $add, []); + } + } + + // Updating things... + $updates = []; + foreach ($_POST['reacts'] as $id => $name) { + // Again, no funny stuff... + $name = trim($name); + + // Did they update this one? Ignore empty ones for now + if ($reactions[$id] != $name && !empty($name)) { + $updates[] = [$id, $name]; + } + } + + // Anything to update? + if (!empty($updates)) { + $do_update = true; + // Do the update + Db::$db->insert('replace', '{db_prefix}reactions', ['id_reaction' => 'int', 'name' => 'string'], $updates, ['id_reaction']); + } + } + + // If we updated anything, re-cache everything + if ($do_update) { + // Re-cache the reactions and update the reactions variable so the form will show the changes + CacheApi::put('reactions', null); + $reactions = $this->getReactions(); + CacheApi::put('reactions', $reactions, 480); + } + } + + // Set up the form now... + + // Create our token + SecurityToken::create('admin-mre'); + + // Set up our list. Use a special function for the get_items so we can output things in input fields... + $listOptions = [ + 'id' => 'reactions_list', + 'title' => Lang::$txt['reactions'], + 'no_items_label' => Lang::$txt['no_reactions'], + 'base_href' => Config::$scripturl . '?action=admin;area=managereactions;sa=edit', + 'get_items' => [ + 'function' => function (int $start, int $items_per_page, string $sort_by) use ($reactions): array { + $items = []; + foreach ($reactions as $id => $name) { + $items[] = [ + 'id' => $id, + 'name' => $name, + ]; + } + return $items; + }, + ], + 'get_count' => [ + 'value' => count($reactions), + ], + 'columns' => + [ + 'name' => + [ + 'header' => [ + 'value' => Lang::$txt['reactions_name'], + ], + 'data' => [ + 'function' => function ($rowData) { + return ''; + } + ], + ], + 'check' => [ + 'header' => [ + 'value' => '', + 'class' => 'centercol', + ], + 'data' => [ + 'function' => function ($rowData) { + return ''; + }, + 'class' => 'centercol', + ] + ] + ] + ]; + + // Add a row for a blank field to add a reaction, and a link to add another blank field. + $listOptions['additional_rows'] = [ + [ + 'position' => 'below_table_data', + 'value' => '' + ], + [ + // Clicking this magic button adds a new row... + 'position' => 'below_table_data', + 'value' => '' + ], + [ + // And last but not least our input buttons + 'position' => 'below_table_data', + 'value' => ' + ' + ] + ]; + + // And some inline JS to handle adding another row + $listOptions['javascript'] = ' + function addrow() { + reacts_table = document.getElementById(\'reactions_list\'); + new_row = document.getElementById(\'reactions_list\').insertRow(reacts_table.rows.length); + new_row.insertCell(0).innerHTML = \'\'; + new_row.insertCell(1).innerHTML = \'\'; + }'; + + // Now that we have our list options set up, have some fun... + $listOptions['form'] = [ + 'href' => Config::$scripturl . '?action=admin;area=managereactions;sa=edit;' . Utils::$context['session_var'] . '=' . Utils::$context['session_id'], + 'name' => 'list_reactions', + 'token' => 'admin-mre', + ]; + + new ItemList($listOptions); + + Utils::$context['page_title'] = Lang::$txt['reactions_manage']; + Utils::$context['sub_template'] = 'show_list'; + Utils::$context['default_list'] = 'reactions_list'; + } + + /****************** + * Internal methods + ******************/ + + /** + * Constructor. Protected to force instantiation via self::load(). + */ + protected function __construct() + { + // Load up our language and set up the menu. + Lang::load('ManageReactions'); + + // Setup the admin tabs. + Menu::$loaded['admin']->tab_data = [ + 'title' => Lang::$txt['reactions'], + 'help' => 'manage_reactions', + 'description' => Lang::$txt['admin_manage_reactions'], + 'tabs' => [ + 'settings' => [ + 'description' => Lang::$txt['reaction_settings_explain'], + ], + 'edit' => [ + 'description' => Lang::$txt['manage_reactions_desc'], + 'disabled' => !Config::$modSettings['enable_reacts'], + ], + ], + ]; + + Utils::$context['last_tab'] = Config::$modSettings['enable_reacts'] ? 'edit' : 'settings'; + + if (!empty($_REQUEST['sa']) && isset(self::$subactions[$_REQUEST['sa']])) { + $this->subaction = $_REQUEST['sa']; + } + + Utils::$context['sub_action'] = &$this->subaction; + } +} +?> \ No newline at end of file diff --git a/Sources/Actions/Display.php b/Sources/Actions/Display.php index a916a4947a..2ce63a33c2 100644 --- a/Sources/Actions/Display.php +++ b/Sources/Actions/Display.php @@ -1144,7 +1144,7 @@ protected function getMessagesAndPosters(): void } /** - * Initializes Msg::get() and loads attachments and likes. + * Initializes Msg::get() and loads attachments and reactions. */ protected function initDisplayContext(): void { @@ -1159,9 +1159,9 @@ protected function initDisplayContext(): void Attachment::prepareByMsg($this->messages); } - // And the likes - if (!empty(Config::$modSettings['enable_likes'])) { - Utils::$context['my_likes'] = Topic::$info->getLikedMsgs(); + // And the reactions + if (!empty(Config::$modSettings['enable_reacts'])) { + Utils::$context['my_reactions'] = Topic::$info->getReactedMsgs(); } // Go to the last message if the given time is beyond the time of the last message. @@ -1181,7 +1181,7 @@ protected function initDisplayContext(): void Msg::$getter = []; Utils::$context['first_message'] = 0; Utils::$context['first_new_message'] = false; - Utils::$context['likes'] = []; + Utils::$context['reactions'] = []; } // Set the callback. (do you REALIZE how much memory all the messages would take?!?) diff --git a/Sources/Actions/Like.php b/Sources/Actions/React.php similarity index 74% rename from Sources/Actions/Like.php rename to Sources/Actions/React.php index cba745aa8d..64a04109bc 100644 --- a/Sources/Actions/Like.php +++ b/Sources/Actions/React.php @@ -23,6 +23,7 @@ use SMF\Db\DatabaseApi as Db; use SMF\IntegrationHook; use SMF\Lang; +use SMF\ReactionTrait; use SMF\Theme; use SMF\Time; use SMF\User; @@ -31,9 +32,10 @@ /** * Handles liking posts and displaying the list of who liked a post. */ -class Like implements ActionInterface +class React implements ActionInterface { use ActionTrait; + use ReactionTrait; /******************* * Public properties @@ -45,7 +47,7 @@ class Like implements ActionInterface * The requested sub-action. * This should be set by the constructor. */ - public string $subaction = 'like'; + public string $subaction = 'react'; /************************** * Public static properties @@ -62,7 +64,7 @@ class Like implements ActionInterface * regarding hooks, etc., assumes that they are only called by like(). */ public static array $subactions = [ - 'like' => 'like', + 'react' => 'react', 'view' => 'view', 'delete' => 'delete', 'insert' => 'insert', @@ -117,14 +119,14 @@ class Like implements ActionInterface * * The number of times the content has been liked. */ - protected int $num_likes = 0; + protected int $num_reacts = 0; /** * @var bool * * If the current user has already liked this content. */ - protected bool $already_liked = false; + protected bool $already_reacted = false; /** * @var array @@ -132,15 +134,15 @@ class Like implements ActionInterface * Mostly used for external integration. Needs to be filled as an array * with the following keys: * - * 'can_like' bool|string True if the current user can actually like + * 'can_react' bool|string True if the current user can actually react to * this content, or a Lang::$txt key for an * error message if not. * - * 'redirect' string URL to redirect to after the like is submitted. + * 'redirect' string URL to redirect to after the react is submitted. * If not set, will redirect to the forum index. * * 'type' string 6 character unique identifier for the content. - * Must match what was sent in $_GET['ltype'] + * Must match what was sent in $_GET['rtype'] * * 'flush_cache' bool If true, reset the like content's cache entry * after a new entry has been inserted. Optional. @@ -153,8 +155,8 @@ class Like implements ActionInterface * 'json' bool If true, the class will return a JSON object as * a response instead of HTML. Default: false. */ - protected array $valid_likes = [ - 'can_like' => false, + protected array $valid_reacts = [ + 'can_react' => false, 'redirect' => '', 'type' => '', 'flush_cache' => '', @@ -163,7 +165,7 @@ class Like implements ActionInterface ]; /** - * @var int + * @var int * * The topic ID. Used for liking messages. */ @@ -186,6 +188,18 @@ class Like implements ActionInterface */ protected mixed $data; + /** + * @var int + * + * The ID of the selected reaction. Should match an entry in the reactions table. + */ + protected int $id_react = 0; + + /** @var array + * + * An array of available reactions + */ + /**************** * Public methods ****************/ @@ -264,10 +278,11 @@ protected function __construct() $this->subaction = $_REQUEST['sa']; } - $this->type = $_GET['ltype'] ?? ''; - $this->content = (int) ($_GET['like'] ?? 0); + $this->type = $_GET['rtype'] ?? ''; + $this->content = (int) ($_GET['react'] ?? 0); $this->js = isset($_GET['js']); $this->extra = $_GET['extra'] ?? false; + $this->id_react = $_GET['id_react'] ?? 0; // We do not want to output debug information here. if ($this->js) { @@ -278,14 +293,14 @@ protected function __construct() /** * Performs basic checks on the data provided, checks for a valid msg like. * - * Calls integrate_valid_likes hook for retrieving all the data needed and + * Calls integrate_valid_reacts hook for retrieving all the data needed and * apply checks based on the data provided. */ protected function check(): void { // This feature is currently disable. - if (empty(Config::$modSettings['enable_likes'])) { - $this->error = 'like_disable'; + if (empty(Config::$modSettings['enable_reacts'])) { + $this->error = 'react_disable'; return; } @@ -301,6 +316,13 @@ protected function check(): void return; } + // Is this a valid reaction ID? + if ($this->id_react != 0 && in_array($this->id_react, $this->getReactions())) { + $this->error = 'invalid_reaction'; + + return; + } + // First we need to verify whether the user can see the type of content. // This is set up to be extensible, so we'll check for the one type we // do know about, and if it's not that, we'll defer to any hooks. @@ -336,48 +358,48 @@ protected function check(): void // So we know what topic it's in and more importantly we know the // user can see it. If we're not viewing, we need some info set up. - $this->valid_likes['type'] = 'msg'; - $this->valid_likes['flush_cache'] = 'likes_topic_' . $this->id_topic . '_' . User::$me->id; - $this->valid_likes['redirect'] = 'topic=' . $this->id_topic . '.msg' . $this->content . '#msg' . $this->content; + $this->valid_reacts['type'] = 'msg'; + $this->valid_reacts['flush_cache'] = 'reacts_topic_' . $this->id_topic . '_' . User::$me->id; + $this->valid_reacts['redirect'] = 'topic=' . $this->id_topic . '.msg' . $this->content . '#msg' . $this->content; - $this->valid_likes['can_like'] = (User::$me->id == $topicOwner ? 'cannot_like_content' : (User::$me->allowedTo('likes_like') ? true : 'cannot_like_content')); + $this->valid_reacts['can_react'] = (User::$me->id == $topicOwner ? 'cannot_react_content' : (User::$me->allowedTo('reacts_react') ? true : 'cannot_react_content')); } else { /* * MOD AUTHORS: This will give you whatever the user offers up in - * terms of liking, e.g. $this->type=msg, $this->content=1. + * terms of reacting, e.g. $this->type=msg, $this->content=1. * * When you hook this, check $this->type first. If it is not * something your mod worries about, return false. * * Otherwise, return an array according to the documentation for - * $this->valid_likes. Determine (however you need to) that the user + * $this->valid_reacts. Determine (however you need to) that the user * can see and can_like the relevant liked content (and it exists). * Remember that users can't like their own content. * * If the user can like it, you MUST return your type in the 'type' * key of the returned array. * - * See also issueLike() for further notes. + * See also issueReact() for further notes. */ - $can_like = IntegrationHook::call('integrate_valid_likes', [$this->type, $this->content, $this->subaction, $this->js, $this->extra]); + $can_react = IntegrationHook::call('integrate_valid_reacts', [$this->type, $this->content, $this->subaction, $this->js, $this->extra]); $found = false; - if (!empty($can_like)) { - $can_like = (array) $can_like; + if (!empty($can_react)) { + $can_react = (array) $can_react; - foreach ($can_like as $result) { + foreach ($can_react as $result) { if ($result !== false) { // Match the type with what we already have. if (!isset($result['type']) || $result['type'] != $this->type) { - $this->error = 'not_valid_like_type'; + $this->error = 'not_valid_react_type'; return; } // Fill out the rest. $this->type = $result['type']; - $this->valid_likes = array_merge($this->valid_likes, $result); + $this->valid_reacts = array_merge($this->valid_reacts, $result); $found = true; break; @@ -392,10 +414,10 @@ protected function check(): void } } - // Is the user able to like this? - // Viewing a list of likes doesn't require this permission. - if ($this->subaction != 'view' && isset($this->valid_likes['can_like']) && is_string($this->valid_likes['can_like'])) { - $this->error = $this->valid_likes['can_like']; + // Is the user able to react to this? + // Viewing a list of reactions doesn't require this permission. + if ($this->subaction != 'view' && isset($this->valid_reacts['can_react']) && is_string($this->valid_reacts['can_react'])) { + $this->error = $this->valid_reacts['can_react']; return; } @@ -408,13 +430,13 @@ protected function delete(): void { Db::$db->query( '', - 'DELETE FROM {db_prefix}user_likes - WHERE content_id = {int:like_content} - AND content_type = {string:like_type} + 'DELETE FROM {db_prefix}user_reacts + WHERE content_id = {int:react_content} + AND content_type = {string:react_type} AND id_member = {int:id_member}', [ - 'like_content' => $this->content, - 'like_type' => $this->type, + 'react_content' => $this->content, + 'react_type' => $this->type, 'id_member' => User::$me->id, ], ); @@ -427,15 +449,15 @@ protected function delete(): void // Check to see if there is an unread alert to delete as well... Alert::deleteWhere( [ - 'content_id = {int:like_content}', - 'content_type = {string:like_type}', + 'content_id = {int:react_content}', + 'content_type = {string:react_type}', 'id_member_started = {int:id_member_started}', 'content_action = {string:content_action}', 'is_read = {int:unread}', ], [ - 'like_content' => $this->content, - 'like_type' => $this->type, + 'react_content' => $this->content, + 'react_type' => $this->type, 'id_member_started' => User::$me->id, 'content_action' => 'like', 'unread' => 0, @@ -444,7 +466,7 @@ protected function delete(): void } /** - * Inserts a new entry on user_likes table. + * Inserts a new entry on user_reacts table. * Creates a background task for the inserted entry. */ protected function insert(): void @@ -456,24 +478,27 @@ protected function insert(): void $content = $this->content; $user = (array) User::$me; $time = time(); + $id = $this->id_react; - IntegrationHook::call('integrate_issue_like_before', [&$type, &$content, &$user, &$time]); + IntegrationHook::call('integrate_issue_react_before', [&$type, &$content, &$user, &$time, &$id]); // Insert the like. Db::$db->insert( 'insert', - '{db_prefix}user_likes', + '{db_prefix}user_reacts', [ 'content_id' => 'int', 'content_type' => 'string-6', 'id_member' => 'int', - 'like_time' => 'int', + 'react_time' => 'int', + 'id_react' => 'int', ], [ $content, $type, $user['id'], $time, + $id, ], [ 'content_id', @@ -484,7 +509,7 @@ protected function insert(): void // Add a background task to process sending alerts. // MOD AUTHORS: you can add your own background task for your own custom - // like event using the "integrate_issue_like" hook or your callback, + // react event using the "integrate_issue_react" hook or your callback, // both are immediately called after this. if ($this->type == 'msg') { Db::$db->insert( @@ -496,7 +521,7 @@ protected function insert(): void 'claimed_time' => 'int', ], [ - 'SMF\\Tasks\\Likes_Notify', + 'SMF\\Tasks\\Reacts_Notify', Utils::jsonEncode([ 'content_id' => $content, 'content_type' => $type, @@ -517,37 +542,37 @@ protected function insert(): void } /** - * Sets $this->num_likes to the actual number of likes that the content has. + * Sets $this->num_reacts to the actual number of reactions that the content has. */ protected function count(): void { $request = Db::$db->query( '', 'SELECT COUNT(*) - FROM {db_prefix}user_likes - WHERE content_id = {int:like_content} - AND content_type = {string:like_type}', + FROM {db_prefix}user_reacts + WHERE content_id = {int:react_content} + AND content_type = {string:react_type}', [ - 'like_content' => $this->content, - 'like_type' => $this->type, + 'react_content' => $this->content, + 'react_type' => $this->type, ], ); - list($likes) = Db::$db->fetch_row($request); + list($reacts) = Db::$db->fetch_row($request); Db::$db->free_result($request); - $this->num_likes = (int) $likes; + $this->num_reacts = (int) $reacts; if ($this->subaction == __FUNCTION__) { - $this->data = $this->num_likes; + $this->data = $this->num_reacts; } } /** - * Performs a like action, either like or unlike. + * Performs a reaction action, either react or "unreact" * - * Counts the total of likes and calls a hook after the event. + * Counts the total of reactions and calls a hook after the event. */ - protected function like(): void + protected function react(): void { // Safety first! if (empty($this->type) || empty($this->content)) { @@ -556,24 +581,24 @@ protected function like(): void return; } - // Do we already like this? + // Did we already react to this? $request = Db::$db->query( '', 'SELECT content_id, content_type, id_member - FROM {db_prefix}user_likes - WHERE content_id = {int:like_content} - AND content_type = {string:like_type} + FROM {db_prefix}user_reacts + WHERE content_id = {int:react_content} + AND content_type = {string:react_type} AND id_member = {int:id_member}', [ - 'like_content' => $this->content, - 'like_type' => $this->type, + 'react_content' => $this->content, + 'react_type' => $this->type, 'id_member' => User::$me->id, ], ); - $this->already_liked = Db::$db->num_rows($request) != 0; + $this->already_reacted = Db::$db->num_rows($request) != 0; Db::$db->free_result($request); - if ($this->already_liked) { + if ($this->already_reacted) { $this->delete(); } else { $this->insert(); @@ -588,104 +613,105 @@ protected function like(): void Db::$db->query( '', 'UPDATE {db_prefix}messages - SET likes = {int:num_likes} + SET reacts = {int:num_reacts} WHERE id_msg = {int:id_msg}', [ 'id_msg' => $this->content, - 'num_likes' => $this->num_likes, + 'num_reacts' => $this->num_reacts, ], ); } // Any callbacks? - elseif (!empty($this->valid_likes['callback'])) { - $call = Utils::getCallable($this->valid_likes['callback']); + elseif (!empty($this->valid_reacts['callback'])) { + $call = Utils::getCallable($this->valid_reacts['callback']); if (!empty($call)) { call_user_func_array($call, [$this]); } } - // Sometimes there might be other things that need updating after we do this like. - IntegrationHook::call('integrate_issue_like', [$this]); + // Sometimes there might be other things that need updating after we do this reaction. + IntegrationHook::call('integrate_issue_react', [$this]); // Now some clean up. This is provided here for any like handlers that // want to do any cache flushing. - // This way a like handler doesn't need to explicitly declare anything - // in integrate_issue_like, but do so in integrate_valid_likes where it + // This way a reaction handler doesn't need to explicitly declare anything + // in integrate_issue_react, but do so in integrate_valid_reacts where it // absolutely has to exist. - if (!empty($this->valid_likes['flush_cache'])) { - CacheApi::put($this->valid_likes['flush_cache'], null); + if (!empty($this->valid_reacts['flush_cache'])) { + CacheApi::put($this->valid_reacts['flush_cache'], null); } // All done, start building the data to pass as response. $this->data = [ 'id_topic' => !empty($this->id_topic) ? $this->id_topic : 0, 'id_content' => $this->content, - 'count' => $this->num_likes, - 'can_like' => $this->valid_likes['can_like'], - 'already_liked' => empty($this->already_liked), + 'count' => $this->num_reacts, + 'can_react' => $this->valid_reacts['can_react'], + 'already_reacted' => empty($this->already_reacted), 'type' => $this->type, ]; } /** - * This is for viewing the people who liked a thing. + * This is for viewing the people who reacted to a thing. * * Accessed from index.php?action=likes;view and should generally load in a * popup. * * We use a template for this in case themers want to style it. + * @TODO: Handle filtering by reaction */ protected function view(): void { // Firstly, load what we need. We already know we can see this, so that's something. - Utils::$context['likers'] = []; + Utils::$context['reactors'] = []; $request = Db::$db->query( '', - 'SELECT id_member, like_time - FROM {db_prefix}user_likes - WHERE content_id = {int:like_content} - AND content_type = {string:like_type} - ORDER BY like_time DESC', + 'SELECT id_member, react_time, id_react + FROM {db_prefix}user_reacts + WHERE content_id = {int:react_content} + AND content_type = {string:react_type} + ORDER BY react_time DESC', [ - 'like_content' => $this->content, - 'like_type' => $this->type, + 'react_content' => $this->content, + 'react_type' => $this->type, ], ); while ($row = Db::$db->fetch_assoc($request)) { - Utils::$context['likers'][$row['id_member']] = ['timestamp' => $row['like_time']]; + Utils::$context['reactors'][$row['id_member']] = ['timestamp' => $row['react_time'], 'id_react' => $row['id_react']]; } Db::$db->free_result($request); // Now to get member data, including avatars and so on. - $members = array_keys(Utils::$context['likers']); + $members = array_keys(Utils::$context['reactors']); $loaded = User::load($members); if (count($loaded) != count($members)) { $members = array_diff($members, array_map(fn ($member) => $member->id, $loaded)); foreach ($members as $not_loaded) { - unset(Utils::$context['likers'][$not_loaded]); + unset(Utils::$context['reactors'][$not_loaded]); } } - foreach (Utils::$context['likers'] as $liker => $dummy) { - if (!isset(User::$loaded[$liker])) { - unset(Utils::$context['likers'][$liker]); + foreach (Utils::$context['reactors'] as $reactor => $dummy) { + if (!isset(User::$loaded[$reactor])) { + unset(Utils::$context['reactors'][$reactor]); continue; } - Utils::$context['likers'][$liker]['profile'] = User::$loaded[$liker]->format(); - Utils::$context['likers'][$liker]['time'] = !empty($dummy['timestamp']) ? Time::create('@' . $dummy['timestamp'])->format() : ''; + Utils::$context['reactors'][$reactor]['profile'] = User::$loaded[$reactor]->format(); + Utils::$context['reactors'][$reactor]['time'] = !empty($dummy['timestamp']) ? Time::create('@' . $dummy['timestamp'])->format() : ''; } - Utils::$context['page_title'] = strip_tags(Lang::getTxt('likes_count', ['num' => count(Utils::$context['likers'])])); + Utils::$context['page_title'] = strip_tags(Lang::getTxt('reacts_count', ['num' => count(Utils::$context['reactors'])])); // Lastly, setting up for display. - Theme::loadTemplate('Likes'); + Theme::loadTemplate('Reacts'); Lang::load('Help'); // For the close window button. Utils::$context['template_layers'] = []; Utils::$context['sub_template'] = 'popup'; @@ -707,42 +733,42 @@ protected function respond(): void } // Want a JSON response, do they? - if ($this->valid_likes['json']) { + if ($this->valid_reacts['json']) { $this->sendJsonReponse(); return; } // Set everything up for display. - Theme::loadTemplate('Likes'); + Theme::loadTemplate('Reacts'); Utils::$context['template_layers'] = []; // If there are any errors, process them first. if ($this->error) { // If this is a generic error, set it up good. if ($this->error == 'cannot_') { - $this->error = $this->subaction == 'view' ? 'cannot_view_likes' : 'cannot_like_content'; + $this->error = $this->subaction == 'view' ? 'cannot_view_reacts' : 'cannot_react_content'; } // Is this request coming from an AJAX call? if ($this->js) { Utils::$context['sub_template'] = 'generic'; - Utils::$context['data'] = Lang::$txt[$this->error] ?? Lang::$txt['like_error']; + Utils::$context['data'] = Lang::$txt[$this->error] ?? Lang::$txt['react_error']; } // Nope? Then just do a redirect to whatever URL was provided. else { - Utils::redirectexit(!empty($this->valid_likes['redirect']) ? $this->valid_likes['redirect'] . ';error=' . $this->error : ''); + Utils::redirectexit(!empty($this->valid_reacts['redirect']) ? $this->valid_reacts['redirect'] . ';error=' . $this->error : ''); } return; } - // A like operation. + // A react operation. // Not an AJAX request so send the user back to the previous // location or the main page. if (!$this->js) { - Utils::redirectexit(!empty($this->valid_likes['redirect']) ? $this->valid_likes['redirect'] : ''); + Utils::redirectexit(!empty($this->valid_reacts['redirect']) ? $this->valid_reacts['redirect'] : ''); } // These fine gentlemen all share the same template. @@ -750,7 +776,7 @@ protected function respond(): void if (in_array($this->subaction, $generic)) { Utils::$context['sub_template'] = 'generic'; - Utils::$context['data'] = Lang::$txt['like_' . $this->data] ?? $this->data; + Utils::$context['data'] = Lang::$txt['react_' . $this->data] ?? $this->data; } // Directly pass the current called sub-action and the data // generated by its associated Method. @@ -772,14 +798,14 @@ protected function sendJsonReponse(): void // If there is an error, send it. if ($this->error) { if ($this->error == 'cannot_') { - $this->error = $this->subaction == 'view' ? 'cannot_view_likes' : 'cannot_like_content'; + $this->error = $this->subaction == 'view' ? 'cannot_view_reacts' : 'cannot_react_content'; } $print['error'] = $this->error; } // Do you want to add something at the very last minute? - IntegrationHook::call('integrate_likes_json_response', [&$print]); + IntegrationHook::call('integrate_reacts_json_response', [&$print]); // Print the data. Utils::serverResponse(Utils::jsonEncode($print)); diff --git a/Sources/Actions/Stats.php b/Sources/Actions/Stats.php index 11a5d13b2c..39aece123c 100644 --- a/Sources/Actions/Stats.php +++ b/Sources/Actions/Stats.php @@ -608,26 +608,26 @@ public function execute(): void CacheApi::put('stats_total_time_members', $temp2, 480); } - // Likes. - if (!empty(Config::$modSettings['enable_likes'])) { + // Reactions. + if (!empty(Config::$modSettings['enable_reacts'])) { // Liked messages top 10. - Utils::$context['stats_blocks']['liked_messages'] = []; - $max_liked_message = 1; - $liked_messages = Db::$db->query( + Utils::$context['stats_blocks']['reacted_messages'] = []; + $max_reacted_message = 1; + $reacted_messages = Db::$db->query( '', - 'SELECT m.id_msg, m.subject, m.likes, m.id_board, m.id_topic, t.approved + 'SELECT m.id_msg, m.subject, m.reacts, m.id_board, m.id_topic, t.approved FROM ( - SELECT n.id_msg, n.subject, n.likes, n.id_board, n.id_topic + SELECT n.id_msg, n.subject, n.reacts, n.id_board, n.id_topic FROM {db_prefix}messages as n - ORDER BY n.likes DESC + ORDER BY n.reacts DESC LIMIT 1000 ) AS m INNER JOIN {db_prefix}topics AS t ON (m.id_topic = t.id_topic) INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board' . (!empty(Config::$modSettings['recycle_enable']) && Config::$modSettings['recycle_board'] > 0 ? ' AND b.id_board != {int:recycle_board}' : '') . ') - WHERE m.likes > 0 AND {query_see_board}' . (Config::$modSettings['postmod_active'] ? ' + WHERE m.reacts > 0 AND {query_see_board}' . (Config::$modSettings['postmod_active'] ? ' AND t.approved = {int:is_approved}' : '') . ' - ORDER BY m.likes DESC + ORDER BY m.reacts DESC LIMIT 10', [ 'recycle_board' => Config::$modSettings['recycle_board'], @@ -635,35 +635,35 @@ public function execute(): void ], ); - while ($row_liked_message = Db::$db->fetch_assoc($liked_messages)) { - Lang::censorText($row_liked_message['subject']); + while ($row_reacted_message = Db::$db->fetch_assoc($reacted_messages)) { + Lang::censorText($row_reacted_message['subject']); - Utils::$context['stats_blocks']['liked_messages'][] = [ - 'id' => $row_liked_message['id_topic'], - 'subject' => $row_liked_message['subject'], - 'num' => $row_liked_message['likes'], - 'href' => Config::$scripturl . '?msg=' . $row_liked_message['id_msg'], - 'link' => '' . $row_liked_message['subject'] . '', + Utils::$context['stats_blocks']['reacted_messages'][] = [ + 'id' => $row_reacted_message['id_topic'], + 'subject' => $row_reacted_message['subject'], + 'num' => $row_reacted_message['reacts'], + 'href' => Config::$scripturl . '?msg=' . $row_reacted_message['id_msg'], + 'link' => '' . $row_reacted_message['subject'] . '', ]; - if ($max_liked_message < $row_liked_message['likes']) { - $max_liked_message = $row_liked_message['likes']; + if ($max_reacted_message < $row_reacted_message['reacts']) { + $max_reacted_message = $row_reacted_message['reacts']; } } - Db::$db->free_result($liked_messages); + Db::$db->free_result($reacted_messages); - foreach (Utils::$context['stats_blocks']['liked_messages'] as $i => $liked_messages) { - Utils::$context['stats_blocks']['liked_messages'][$i]['percent'] = round(($liked_messages['num'] * 100) / $max_liked_message); + foreach (Utils::$context['stats_blocks']['reacted_messages'] as $i => $reacted_messages) { + Utils::$context['stats_blocks']['reacted_messages'][$i]['percent'] = round(($reacted_messages['num'] * 100) / $max_reacted_message); } - // Liked users top 10. - Utils::$context['stats_blocks']['liked_users'] = []; - $max_liked_users = 1; - $liked_users = Db::$db->query( + // Reacted users top 10. + Utils::$context['stats_blocks']['reacted_users'] = []; + $max_reacted_users = 1; + $reacted_users = Db::$db->query( '', - 'SELECT m.id_member AS liked_user, COUNT(l.content_id) AS count, mem.real_name - FROM {db_prefix}user_likes AS l - INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg) + 'SELECT m.id_member AS reacted_user, COUNT(r.content_id) AS count, mem.real_name + FROM {db_prefix}user_reacts AS r + INNER JOIN {db_prefix}messages AS m ON (r.content_id = m.id_msg) INNER JOIN {db_prefix}members AS mem ON (m.id_member = mem.id_member) WHERE content_type = {literal:msg} AND m.id_member > {int:zero} @@ -676,24 +676,24 @@ public function execute(): void ], ); - while ($row_liked_users = Db::$db->fetch_assoc($liked_users)) { - Utils::$context['stats_blocks']['liked_users'][] = [ - 'id' => $row_liked_users['liked_user'], - 'num' => $row_liked_users['count'], - 'href' => Config::$scripturl . '?action=profile;u=' . $row_liked_users['liked_user'], - 'name' => $row_liked_users['real_name'], - 'link' => '' . $row_liked_users['real_name'] . '', + while ($row_reacted_users = Db::$db->fetch_assoc($reacted_users)) { + Utils::$context['stats_blocks']['reacted_users'][] = [ + 'id' => $row_reacted_users['reacted_user'], + 'num' => $row_reacted_users['count'], + 'href' => Config::$scripturl . '?action=profile;u=' . $row_reacted_users['reacted_user'], + 'name' => $row_reacted_users['real_name'], + 'link' => '' . $row_reacted_users['real_name'] . '', ]; - if ($max_liked_users < $row_liked_users['count']) { - $max_liked_users = $row_liked_users['count']; + if ($max_reacted_users < $row_reacted_users['count']) { + $max_reacted_users = $row_reacted_users['count']; } } - Db::$db->free_result($liked_users); + Db::$db->free_result($reacted_users); - foreach (Utils::$context['stats_blocks']['liked_users'] as $i => $liked_users) { - Utils::$context['stats_blocks']['liked_users'][$i]['percent'] = round(($liked_users['num'] * 100) / $max_liked_users); + foreach (Utils::$context['stats_blocks']['reacted_users'] as $i => $reacted_users) { + Utils::$context['stats_blocks']['reacted_users'][$i]['percent'] = round(($reacted_users['num'] * 100) / $max_reacted_users); } } diff --git a/Sources/Forum.php b/Sources/Forum.php index d3b8a70d00..4b9812ee68 100644 --- a/Sources/Forum.php +++ b/Sources/Forum.php @@ -68,7 +68,7 @@ class Forum 'helpadmin' => ['', 'SMF\\Actions\\HelpAdmin::call'], 'jsmodify' => ['', 'SMF\\Actions\\JavaScriptModify::call'], 'jsoption' => ['', 'SMF\\Theme::setJavaScript'], - 'likes' => ['', 'SMF\\Actions\\Like::call'], + 'likes' => ['', 'SMF\\Actions\\React::call'], 'lock' => ['', 'SMF\\Topic::lock'], 'lockvoting' => ['', 'SMF\\Poll::lock'], 'login' => ['', 'SMF\\Actions\\Login::call'], @@ -112,7 +112,7 @@ class Forum 'sticky' => ['', 'SMF\\Topic::sticky'], 'theme' => ['', 'SMF\\Theme::dispatch'], 'trackip' => ['', 'SMF\\Actions\\TrackIP::call'], - 'about:unknown' => ['', 'SMF\\Actions\\Like::BookOfUnknown'], + 'about:unknown' => ['', 'SMF\\Actions\\React::BookOfUnknown'], 'unread' => ['', 'SMF\\Actions\\Unread::call'], 'unreadreplies' => ['', 'SMF\\Actions\\UnreadReplies::call'], 'uploadAttach' => ['', 'SMF\\Actions\\AttachmentUpload::call'], diff --git a/Sources/Msg.php b/Sources/Msg.php index 0c2bf33e76..4c1cc0f8ac 100644 --- a/Sources/Msg.php +++ b/Sources/Msg.php @@ -160,9 +160,9 @@ class Msg implements \ArrayAccess /** * @var int * - * The number of likes this message has received. + * The number of reactions this message has received. */ - public int $likes = 0; + public int $reactions = 0; /** * @var bool @@ -206,6 +206,13 @@ class Msg implements \ArrayAccess */ public static $getter; + /** + * @var array + * + * Variable to hold info about how many of each reaction we have + */ + public static $reacts_count = []; + /********************* * Internal properties *********************/ @@ -309,6 +316,7 @@ public function format(int $counter = 0, array $format_options = []): array 'new' => empty($this->is_read), 'first_new' => isset(Utils::$context['start_from']) && Utils::$context['start_from'] == $counter, 'is_ignored' => !empty(Config::$modSettings['enable_buddylist']) && !empty(Theme::$current->options['posts_apply_ignore_list']) && in_array($this->id_member, User::$me->ignoreusers), + 'num_reactions' => (int) $this->reactions, ]; // Are we showing the icon? @@ -468,15 +476,38 @@ public function format(int $counter = 0, array $format_options = []): array $this->formatted['short_subject'] = Utils::shorten($this->formatted['subject'], $format_options['shorten_subject']); } - // Are likes enabled? - if (!empty(Config::$modSettings['enable_likes'])) { - $this->formatted['likes'] = [ - 'count' => $this->likes, - 'you' => in_array($this->id, Utils::$context['my_likes'] ?? []), - ]; + // Are reactions enabled? + if (!empty(Config::$modSettings['enable_reacts'])) { + $this->formatted['reacts'] = [ + 'count' => $this->reactions, + 'you' => in_array($this->id, Utils::$context['my_reactions'] ?? []), + ];; + + if ($this->reactions != 0) { + // Load up the number of each type of reactions + $query = Db::$db->query( + '', + 'SELECT id_react, COUNT(*) AS num_reacts + FROM {db_prefix}user_reacts + WHERE content_type = {string:content_type} + AND content_id = {int:content_id} + GROUP BY id_react + ORDER BY num_reacts DESC', + [ + 'content_type' => 'msg', + 'content_id' => $this->id, + ] + ); + + // Loop through the results + while ($row = Db::$db->fetchAssoc($query)) { + $this->reactions[$row['id_react']] = $row['num_reacts']; + } + Db::$db->freeResult($query); + } if ($format_options['do_permissions']) { - $this->formatted['likes']['can_like'] = !User::$me->is_guest && $this->id_member != User::$me->id && !empty($topic->permissions['can_like']); + $this->formatted['reactions']['can_react'] = !User::$me->is_guest && $this->id_member != User::$me->id && !empty($topic->permissions['can_react']); } } @@ -2763,6 +2794,17 @@ public static function remove(int $message, bool $decreasePostCount = true): boo ], ); + // Drop any reactions related to this post. We can recalculate stats later + Db::$db->query( + '', + 'DELETE FROM {db_prefix}user_reacts + WHERE content_type = {string:msg} AND content_id = {int:id_msg}', + [ + 'msg' => 'msg', + 'id_msg' => $message, + ], + ); + // Delete attachment(s) if they exist. $attachmentQuery = [ 'attachment_type' => 0, diff --git a/Sources/ReactionTrait.php b/Sources/ReactionTrait.php new file mode 100644 index 0000000000..485ae78a7f --- /dev/null +++ b/Sources/ReactionTrait.php @@ -0,0 +1,50 @@ +query( + '', + 'SELECT * FROM {db_prefix}reactions', + []); + + while ($result = Db::$db->fetch_assoc($request)) { + $reactions[$result['id_reaction']] = $result['name']; + } + + Db::$db->free_result($request); + + // Cache the results + CacheApi::put('reactions', $reactions, 480); + } + return $reactions; + } +} \ No newline at end of file diff --git a/Sources/Topic.php b/Sources/Topic.php index 2c3e64242d..ff8a31a397 100644 --- a/Sources/Topic.php +++ b/Sources/Topic.php @@ -495,30 +495,30 @@ public function getNotificationPrefs(): array } /** - * Gets the IDs of messages in this topic that the current user likes. + * Gets the IDs of messages in this topic that the current user reacted to + * as well as the ID of the chosen reaction for each message. * - * @param int $topic The topic ID to fetch the info from. - * @return array IDs of messages in this topic that the current user likes. + * @return array An array of arrays each containing the ID of a reacted post and ID of the reaction */ - public function getLikedMsgs(): array + public function getReactedMsgs(): array { if (User::$me->is_guest) { return []; } - $cache_key = 'likes_topic_' . $this->id . '_' . User::$me->id; + $cache_key = 'reacts_topic_' . $this->id . '_' . User::$me->id; $ttl = 180; - if (($liked_messages = CacheApi::get($cache_key, $ttl)) === null) { - $liked_messages = []; + if (($reacted_messages = CacheApi::get($cache_key, $ttl)) === null) { + $reacted_messages = []; $request = Db::$db->query( '', - 'SELECT content_id - FROM {db_prefix}user_likes AS l - INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg) - WHERE l.id_member = {int:current_user} - AND l.content_type = {literal:msg} + 'SELECT r.content_id, r.id_reaction + FROM {db_prefix}user_reacts AS r + INNER JOIN {db_prefix}messages AS m ON (r.content_id = m.id_msg) + WHERE r.id_member = {int:current_user} + AND r.content_type = {literal:msg} AND m.id_topic = {int:topic}', [ 'current_user' => User::$me->id, @@ -527,14 +527,14 @@ public function getLikedMsgs(): array ); while ($row = Db::$db->fetch_assoc($request)) { - $liked_messages[] = (int) $row['content_id']; + $reacted_messages[] = [(int) $row['content_id'], (int) $row['id_reaction']]; } Db::$db->free_result($request); - CacheApi::put($cache_key, $liked_messages, $ttl); + CacheApi::put($cache_key, $reacted_messages, $ttl); } - return $liked_messages; + return $reacted_messages; } /** @@ -1389,6 +1389,21 @@ public static function remove(array|int $topics, bool $decreasePostCount = true, Attachment::remove($attachmentQuery, 'messages'); // Delete anything related to the topic. + // Do this first because we need the message IDs... + Db::$db->query( + '', + 'DELETE FROM {db_prefix}user_reacts + WHERE content_type={string:msg} + AND content_id IN + (SELECT id_msg + FROM {db_prefix}messages + WHERE id_topic IN ({array_int:topics}) + )', + [ + 'msg' => 'msg', + 'topics' => $topics, + ] + ); Db::$db->query( '', 'DELETE FROM {db_prefix}messages @@ -1462,9 +1477,9 @@ public static function remove(array|int $topics, bool $decreasePostCount = true, * @param int $topic The topic ID to fetch the info from. * @return array An array of IDs of messages in the specified topic that the current user likes */ - public static function prepareLikesContext(int $topic): array + public static function prepareReactsContext(int $topic): array { - return self::load($topic)->getLikedMsgs(); + return self::load($topic)->getReactedMsgs(); } /****************** diff --git a/Sources/User.php b/Sources/User.php index b5652afc60..bd9bd8bcf1 100644 --- a/Sources/User.php +++ b/Sources/User.php @@ -3365,10 +3365,10 @@ public static function delete(int|array $users, bool $check_not_admin = false): ], ); - // Delete anything they liked. + // Delete anything they reacted to. Db::$db->query( '', - 'DELETE FROM {db_prefix}user_likes + 'DELETE FROM {db_prefix}user_reacts WHERE id_member IN ({array_int:users})', [ 'users' => $users, diff --git a/Themes/default/Likes.template.php b/Themes/default/Reacts.template.php similarity index 97% rename from Themes/default/Likes.template.php rename to Themes/default/Reacts.template.php index 5011ff1fb2..4fb474b5b0 100644 --- a/Themes/default/Likes.template.php +++ b/Themes/default/Reacts.template.php @@ -57,12 +57,12 @@ function template_popup() /** * Display a like button and info about how many people liked something */ -function template_like() +function template_react() { echo '