From 130cca44ac15a7da6f4b14b07332b830ea996286 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sat, 27 Apr 2024 00:30:21 -0400 Subject: [PATCH 01/77] Initial commit to change likes to reactions... Signed-off-by: Michael Eshom --- Languages/en_US/Alerts.php | 2 +- Languages/en_US/General.php | 33 +++++++++++++++------------ Languages/en_US/ManagePermissions.php | 6 ++--- Languages/en_US/Profile.php | 2 +- other/install_3-0_MySQL.sql | 15 ++++++------ other/install_3-0_PostgreSQL.sql | 19 +++++++-------- 6 files changed, 41 insertions(+), 36 deletions(-) diff --git a/Languages/en_US/Alerts.php b/Languages/en_US/Alerts.php index 04f1ab98e7..e9be4d47be 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 905a812c32..fd404b13ce 100644 --- a/Languages/en_US/General.php +++ b/Languages/en_US/General.php @@ -845,24 +845,27 @@ // 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['report_to_mod'] = 'Report to moderator'; diff --git a/Languages/en_US/ManagePermissions.php b/Languages/en_US/ManagePermissions.php index 04af6813b7..0d9b2a070f 100644 --- a/Languages/en_US/ManagePermissions.php +++ b/Languages/en_US/ManagePermissions.php @@ -239,9 +239,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/Profile.php b/Languages/en_US/Profile.php index 822aeea5af..3355ff5e19 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/other/install_3-0_MySQL.sql b/other/install_3-0_MySQL.sql index fca8f7057e..5550db5e03 100644 --- a/other/install_3-0_MySQL.sql +++ b/other/install_3-0_MySQL.sql @@ -805,7 +805,7 @@ CREATE TABLE {$db_prefix}messages ( body TEXT NOT NULL, icon VARCHAR(16) NOT NULL DEFAULT 'xx', approved TINYINT NOT NULL DEFAULT '1', - likes SMALLINT UNSIGNED NOT NULL DEFAULT '0', + reacts SMALLINT UNSIGNED NOT NULL DEFAULT '0', PRIMARY KEY (id_msg), UNIQUE idx_id_board (id_board, id_msg, approved), UNIQUE idx_id_member (id_member, id_msg), @@ -815,7 +815,7 @@ CREATE TABLE {$db_prefix}messages ( INDEX idx_id_member_msg (id_member, approved, id_msg), INDEX idx_current_topic (id_topic, id_msg, id_member, approved), INDEX idx_related_ip (id_member, poster_ip, id_msg), - INDEX idx_likes (likes) + INDEX idx_reacts (reacts) ) ENGINE={$engine}; # @@ -1188,17 +1188,18 @@ CREATE TABLE {$db_prefix}user_drafts ( ) ENGINE={$engine}; # -# Table structure for table `user_likes` +# Table structure for table `user_reacts` # -CREATE TABLE {$db_prefix}user_likes ( +CREATE TABLE {$db_prefix}user_reacts ( id_member MEDIUMINT UNSIGNED DEFAULT '0', content_type CHAR(6) DEFAULT '', content_id INT UNSIGNED DEFAULT '0', - like_time INT UNSIGNED NOT NULL DEFAULT '0', + react_time INT UNSIGNED NOT NULL DEFAULT '0', + react_id INT UNSIGNED DEFAULT '0', PRIMARY KEY (content_id, content_type, id_member), INDEX content (content_id, content_type), - INDEX liker (id_member) + INDEX reactor (id_member) ) ENGINE={$engine}; # @@ -2272,7 +2273,7 @@ VALUES (0, 'alert_timeout', 10), (0, 'member_report', 3), (0, 'member_report_reply', 3), (0, 'msg_auto_notify', 0), - (0, 'msg_like', 1), + (0, 'msg_react', 1), (0, 'msg_mention', 1), (0, 'msg_notify_pref', 1), (0, 'msg_notify_type', 1), diff --git a/other/install_3-0_PostgreSQL.sql b/other/install_3-0_PostgreSQL.sql index 86b7340766..83b117b5d3 100644 --- a/other/install_3-0_PostgreSQL.sql +++ b/other/install_3-0_PostgreSQL.sql @@ -1210,7 +1210,7 @@ CREATE TABLE {$db_prefix}messages ( body text NOT NULL, icon varchar(16) NOT NULL DEFAULT 'xx', approved smallint NOT NULL DEFAULT '1', - likes smallint NOT NULL DEFAULT '0', + reacts smallint NOT NULL DEFAULT '0', PRIMARY KEY (id_msg) ); @@ -1226,7 +1226,7 @@ CREATE INDEX {$db_prefix}messages_show_posts ON {$db_prefix}messages (id_member, CREATE INDEX {$db_prefix}messages_id_member_msg ON {$db_prefix}messages (id_member, approved, id_msg); CREATE INDEX {$db_prefix}messages_current_topic ON {$db_prefix}messages (id_topic, id_msg, id_member, approved); CREATE INDEX {$db_prefix}messages_related_ip ON {$db_prefix}messages (id_member, poster_ip, id_msg); -CREATE INDEX {$db_prefix}messages_likes ON {$db_prefix}messages (likes); +CREATE INDEX {$db_prefix}messages_reacts ON {$db_prefix}messages (reacts); # # Table structure for table `moderators` # @@ -1734,23 +1734,24 @@ CREATE TABLE {$db_prefix}user_drafts ( CREATE UNIQUE INDEX {$db_prefix}user_drafts_id_member ON {$db_prefix}user_drafts (id_member, id_draft, type); # -# Table structure for table `user_likes` +# Table structure for table `user_reacts` # -CREATE TABLE {$db_prefix}user_likes ( +CREATE TABLE {$db_prefix}user_reacts ( id_member int NOT NULL DEFAULT '0', content_type char(6) DEFAULT '', content_id int NOT NULL DEFAULT '0', - like_time int NOT NULL DEFAULT '0', + react_time int NOT NULL DEFAULT '0', + react_id int NOT NULL DEFAULT '0', PRIMARY KEY (content_id, content_type, id_member) ); # -# Indexes for table `user_likes` +# Indexes for table `user_reacts` # -CREATE INDEX {$db_prefix}user_likes_content ON {$db_prefix}user_likes (content_id, content_type); -CREATE INDEX {$db_prefix}user_likes_liker ON {$db_prefix}user_likes (id_member); +CREATE INDEX {$db_prefix}user_reacts_content ON {$db_prefix}user_reacts (content_id, content_type); +CREATE INDEX {$db_prefix}user_reacts_reactorr ON {$db_prefix}user_reacts (id_member); # # Table structure for `mentions` @@ -2827,7 +2828,7 @@ VALUES (0, 'alert_timeout', 10), (0, 'member_report', 3), (0, 'member_report_reply', 3), (0, 'msg_auto_notify', 0), - (0, 'msg_like', 1), + (0, 'msg_react', 1), (0, 'msg_mention', 1), (0, 'msg_notify_pref', 1), (0, 'msg_notify_type', 1), From ab2b1d5124911c495730e981cbd89630edc02b2b Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sun, 28 Apr 2024 16:51:47 -0400 Subject: [PATCH 02/77] Create the reactions table and change the ID column to match SMF standard naming. --- other/install_3-0_MySQL.sql | 21 ++++++++++++++++++++- other/install_3-0_PostgreSQL.sql | 28 ++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/other/install_3-0_MySQL.sql b/other/install_3-0_MySQL.sql index 5550db5e03..8023aa591d 100644 --- a/other/install_3-0_MySQL.sql +++ b/other/install_3-0_MySQL.sql @@ -988,6 +988,16 @@ CREATE TABLE {$db_prefix}qanda ( INDEX idx_lngfile (lngfile) ) ENGINE={$engine}; +# +# Table structure for table `reactions` +# + +CREATE TABLE {$db_prefix}reactions ( + id_reaction SMALLINT UNSIGNED AUTO_INCREMENT, + name VARCHAR(255) NOT NULL DEFAULT '', + PRIMARY KEY (id_reaction) +) ENGINE={$engine}; + # # Table structure for table `scheduled_tasks` # @@ -1193,10 +1203,10 @@ CREATE TABLE {$db_prefix}user_drafts ( CREATE TABLE {$db_prefix}user_reacts ( id_member MEDIUMINT UNSIGNED DEFAULT '0', + id_react SMALLINT UNSIGNED DEFAULT '0', content_type CHAR(6) DEFAULT '', content_id INT UNSIGNED DEFAULT '0', react_time INT UNSIGNED NOT NULL DEFAULT '0', - react_id INT UNSIGNED DEFAULT '0', PRIMARY KEY (content_id, content_type, id_member), INDEX content (content_id, content_type), INDEX reactor (id_member) @@ -1927,6 +1937,15 @@ VALUES (-1, 'search_posts'), (2, 'access_mod_center'); # -------------------------------------------------------- +# +# Dumping data for table `reactions` +# + +INSERT INTO {$db_prefix}reactions + (id_reaction, name) +VALUES + (1, 'like'); + # # Dumping data for table `scheduled_tasks` # diff --git a/other/install_3-0_PostgreSQL.sql b/other/install_3-0_PostgreSQL.sql index 83b117b5d3..f944fb0772 100644 --- a/other/install_3-0_PostgreSQL.sql +++ b/other/install_3-0_PostgreSQL.sql @@ -1462,6 +1462,22 @@ CREATE TABLE {$db_prefix}qanda ( CREATE INDEX {$db_prefix}qanda_lngfile ON {$db_prefix}qanda (lngfile varchar_pattern_ops); +# +# Sequence for table `reactions` +# + +CREATE SEQUENCE {$db_prefix}reactions_seq START WITH 0; + +# +# Table structure for table `reactions` +# + +CREATE TABLE {$db_prefix}reactions ( + id_reaction smallint DEFAULT nextval('{$db_prefix}reactions_seq'), + name varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (id_reaction) +); + # # Sequence for table `scheduled_tasks` # @@ -1739,10 +1755,10 @@ CREATE UNIQUE INDEX {$db_prefix}user_drafts_id_member ON {$db_prefix}user_drafts CREATE TABLE {$db_prefix}user_reacts ( id_member int NOT NULL DEFAULT '0', + id_reaction smallint NOT NULL DEFAULT '0', content_type char(6) DEFAULT '', content_id int NOT NULL DEFAULT '0', react_time int NOT NULL DEFAULT '0', - react_id int NOT NULL DEFAULT '0', PRIMARY KEY (content_id, content_type, id_member) ); @@ -1751,7 +1767,7 @@ CREATE TABLE {$db_prefix}user_reacts ( # CREATE INDEX {$db_prefix}user_reacts_content ON {$db_prefix}user_reacts (content_id, content_type); -CREATE INDEX {$db_prefix}user_reacts_reactorr ON {$db_prefix}user_reacts (id_member); +CREATE INDEX {$db_prefix}user_reacts_reactor ON {$db_prefix}user_reacts (id_member); # # Table structure for `mentions` @@ -2483,6 +2499,14 @@ VALUES (-1, 'search_posts'), (2, 'access_mod_center'); # -------------------------------------------------------- +# +# Dumping data for table `reactions` +# + +INSERT INTO {$db_prefix}reactions + (id_reaction, name) +VALUES (1, 'like'); + # # Dumping data for table `scheduled_tasks` # From 08ca4ad60b54a23219cf3be67a2efb2ec7d3b4ed Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sun, 28 Apr 2024 20:06:44 -0400 Subject: [PATCH 03/77] Handle upgrades - creating/renaming things as needed. --- other/upgrade_2-1_MySQL.sql | 28 -------------- other/upgrade_2-1_PostgreSQL.sql | 24 ------------ other/upgrade_3-0_MySQL.sql | 63 ++++++++++++++++++++++++++++++++ other/upgrade_3-0_PostgreSQL.sql | 63 ++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 52 deletions(-) diff --git a/other/upgrade_2-1_MySQL.sql b/other/upgrade_2-1_MySQL.sql index ac6815f248..9b7da5d3af 100644 --- a/other/upgrade_2-1_MySQL.sql +++ b/other/upgrade_2-1_MySQL.sql @@ -1690,26 +1690,6 @@ VALUES (-1, '1', 'drafts_show_saved_enabled', '1'); ---# -/******************************************************************************/ ---- Adding support for likes -/******************************************************************************/ ----# Creating likes table. -CREATE TABLE IF NOT EXISTS {$db_prefix}user_likes ( - id_member MEDIUMINT UNSIGNED DEFAULT '0', - content_type CHAR(6) DEFAULT '', - content_id INT UNSIGNED DEFAULT '0', - like_time INT UNSIGNED NOT NULL DEFAULT '0', - PRIMARY KEY (content_id, content_type, id_member), - INDEX idx_content (content_id, content_type), - INDEX idx_liker (id_member) -) ENGINE=MyISAM; ----# - ----# Adding likes column to the messages table. (May take a while) -ALTER TABLE {$db_prefix}messages -ADD COLUMN likes SMALLINT UNSIGNED NOT NULL DEFAULT '0'; ----# - /******************************************************************************/ --- Adding support for mentions /******************************************************************************/ @@ -3071,14 +3051,6 @@ UPDATE {$db_prefix}members SET lngfile = REPLACE(lngfile, '-utf8', ''); ---# -/******************************************************************************/ ---- Create index for messages likes -/******************************************************************************/ ----# Add Index for messages likes -DROP INDEX idx_likes ON {$db_prefix}messages; -CREATE INDEX idx_likes ON {$db_prefix}messages (likes); ----# - /******************************************************************************/ --- Aligning legacy column data /******************************************************************************/ diff --git a/other/upgrade_2-1_PostgreSQL.sql b/other/upgrade_2-1_PostgreSQL.sql index 40715bfa9a..39f2bc49da 100644 --- a/other/upgrade_2-1_PostgreSQL.sql +++ b/other/upgrade_2-1_PostgreSQL.sql @@ -1895,30 +1895,6 @@ INSERT INTO {$db_prefix}settings (variable, value) VALUES ('drafts_keep_days', ' INSERT INTO {$db_prefix}themes (id_member, id_theme, variable, value) VALUES (-1, '1', 'drafts_show_saved_enabled', '1') ON CONFLICT DO NOTHING; ---# -/******************************************************************************/ ---- Adding support for likes -/******************************************************************************/ ----# Creating likes table. -CREATE TABLE IF NOT EXISTS {$db_prefix}user_likes ( - id_member int NOT NULL DEFAULT '0', - content_type char(6) DEFAULT '', - content_id int NOT NULL DEFAULT '0', - like_time int NOT NULL DEFAULT '0', - PRIMARY KEY (content_id, content_type, id_member) -); - -DROP INDEX IF EXISTS {$db_prefix}user_likes_content; -DROP INDEX IF EXISTS {$db_prefix}user_likes_liker; - -CREATE INDEX {$db_prefix}user_likes_content ON {$db_prefix}user_likes (content_id, content_type); -CREATE INDEX {$db_prefix}user_likes_liker ON {$db_prefix}user_likes (id_member); ----# - ----# Adding likes column to the messages table. (May take a while) -ALTER TABLE {$db_prefix}messages -ADD COLUMN IF NOT EXISTS likes smallint NOT NULL default '0'; ----# - /******************************************************************************/ --- Adding support for mentions /******************************************************************************/ diff --git a/other/upgrade_3-0_MySQL.sql b/other/upgrade_3-0_MySQL.sql index 45ed54ef59..177061c7e6 100644 --- a/other/upgrade_3-0_MySQL.sql +++ b/other/upgrade_3-0_MySQL.sql @@ -89,3 +89,66 @@ foreach (Config::$modSettings as $variable => $value) { } ---} ---# + +/******************************************************************************/ +--- Adding support for reactions +/******************************************************************************/ +---{ +// Make sure we haven't already done this... +$cols = Db::$db->list_columns('messages'); +// If the reactions column exists in the messages table, there's nothing to do +if(!in_array($cols, 'reactions')) +{ + // Does the user_likes table exist? + $table_exists = Db::$db->list_tables(false, '%user_likes'); + if (!empty($table_exists)) + { + // It already exists. Rename it. + upgrade_query("ALTER TABLE {db_prefix}user_likes RENAME TO {db_prefix}user_reacts"); + + // Add the new column + Db::$db->add_column('{db_prefix}user_reacts', ['name' => 'id_react', 'type' => 'smallint', 'null' => false, default => '0']); + + // Default react type is "like" for now... + upgrade_query("UPDATE {db_prefix}user_reacts SET id_reaction=1"); + + // Rename the like_time column + Db::$db->change_column('{db_prefix}user_reacts', 'like_time', ['name' => 'react_time']); + + // Rename the index + upgrade_query("ALTER TABLE {db_prefix}user_reacts RENAME idx_liker TO idx_reactor"); + + // Rename the likes column in the messages table + Db::$db->change_column('{db_prefix}messages', 'likes', ['name' => 'reactions']); + + // Update user alert prefs + upgrade_query("UPDATE {db_prefix}user_alerts_prefs SET alert_pref='msg_react' WHERE alert_pref='msg_like'"); + } + else + { + // Create the table + upgrade_query(" + CREATE TABLE {db_prefix}user_reacts ( + id_member MEDIUMINT UNSIGNED DEFAULT '0', + id_reaction SMALLINT UNSIGNED DEFAULT '0', + content_type CHAR(6) DEFAULT '', + content_id INT UNSIGNED DEFAULT '0', + react_time INT UNSIGNED NOT NULL DEFAULT '0', + PRIMARY KEY (content_id, content_type, id_member), + INDEX idx_content (content_id, content_type), + INDEX idx_reactor (id_member) + ) ENGINE=InnoDB + "); + } + + // Either way we want to add the new table + upgrade_query(" + CREATE TABLE {db_prefix}reactions ( + id_reaction SMALLINT UNSIGNED DEFAULT '0' AUTO_INCREMENT, + name varchar(255) NOT NULL DEFAULT '' + ) + "); + // Default reaction is "like" + upgrade_query("INSERT INTO {db_prefix}reactions (id_reaction, name) VALUES (1, 'like')"); +} +---} \ No newline at end of file diff --git a/other/upgrade_3-0_PostgreSQL.sql b/other/upgrade_3-0_PostgreSQL.sql index 45ed54ef59..06b2d71e2c 100644 --- a/other/upgrade_3-0_PostgreSQL.sql +++ b/other/upgrade_3-0_PostgreSQL.sql @@ -89,3 +89,66 @@ foreach (Config::$modSettings as $variable => $value) { } ---} ---# + +/******************************************************************************/ +--- Adding support for reactions +/******************************************************************************/ +---{ +// Make sure we haven't already done this... +$cols = Db::$db->list_columns('messages'); +// If the reactions column exists in the messages table, there's nothing to do +if(!in_array($cols, 'reactions')) +{ + // Does the user_likes table exist? + $table_exists = Db::$db->list_tables(false, '%user_likes'); + if (!empty($table_exists)) + { + // It already exists. Rename it. + upgrade_query("ALTER TABLE {db_prefix}user_likes RENAME TO {db_prefix}user_reacts"); + + // Add the new column + Db::$db->add_column('{db_prefix}user_reacts', ['name' => 'id_react', 'type' => 'smallint', 'null' => false, default => '0']); + + // Default react type is "like" for now... + upgrade_query("UPDATE {db_prefix}user_reacts SET id_reaction=1"); + + // Rename the like_time column + Db::$db->change_column('{db_prefix}user_reacts', 'like_time', ['name' => 'react_time']); + + // Rename the index + upgrade_query("ALTER INDEX idx_liker RENAME TO idx_reactor"); + + // Rename the likes column in the messages table + Db::$db->change_column('{db_prefix}messages', 'likes', ['name' => 'reactions']); + + // Update user alert prefs + upgrade_query("UPDATE {db_prefix}user_alerts_prefs SET alert_pref='msg_react' WHERE alert_pref='msg_like'"); + } + else + { + upgrade_query(" + CREATE TABLE {db_prefix}user_reacts ( + id_member mediumint default '0', + id_reaction smallint default '0', + content_type char(6) default '', + content_id int default '0', + react_time int unsigned not null default '0', + PRIMARY KEY (content_id, content_type, id_member), + INDEX idx_content (content_id, content_type), + INDEX idx_reactor (id_member) + ) + "); + } + + // Either way we want to add the new table + upgrade_query("CREATE SEQUENCE {db_prefix}reactions_seq"); + upgrade_query(" + CREATE TABLE {db_prefix}reactions ( + id_reaction smallint default nextval('{db_prefix}reactions_seq'), + name varchar(255) not null default '' + ) + "); + // Default reaction is "like" + upgrade_query("INSERT INTO {db_prefix}reactions (id_reaction, name) VALUES (1, 'like')"); +} +---} \ No newline at end of file From 5714530e50bd5ac6a62c7c34393bc9c9db9947e7 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sun, 28 Apr 2024 21:06:58 -0400 Subject: [PATCH 04/77] Consistency is a good thing... --- other/install_3-0_MySQL.sql | 4 ++-- other/install_3-0_PostgreSQL.sql | 4 ++-- other/upgrade_3-0_MySQL.sql | 8 +++++++- other/upgrade_3-0_PostgreSQL.sql | 7 +++++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/other/install_3-0_MySQL.sql b/other/install_3-0_MySQL.sql index 8023aa591d..94f8868ca8 100644 --- a/other/install_3-0_MySQL.sql +++ b/other/install_3-0_MySQL.sql @@ -805,7 +805,7 @@ CREATE TABLE {$db_prefix}messages ( body TEXT NOT NULL, icon VARCHAR(16) NOT NULL DEFAULT 'xx', approved TINYINT NOT NULL DEFAULT '1', - reacts SMALLINT UNSIGNED NOT NULL DEFAULT '0', + reactions SMALLINT UNSIGNED NOT NULL DEFAULT '0', PRIMARY KEY (id_msg), UNIQUE idx_id_board (id_board, id_msg, approved), UNIQUE idx_id_member (id_member, id_msg), @@ -815,7 +815,7 @@ CREATE TABLE {$db_prefix}messages ( INDEX idx_id_member_msg (id_member, approved, id_msg), INDEX idx_current_topic (id_topic, id_msg, id_member, approved), INDEX idx_related_ip (id_member, poster_ip, id_msg), - INDEX idx_reacts (reacts) + INDEX idx_reactions (reactions) ) ENGINE={$engine}; # diff --git a/other/install_3-0_PostgreSQL.sql b/other/install_3-0_PostgreSQL.sql index f944fb0772..08d8073e33 100644 --- a/other/install_3-0_PostgreSQL.sql +++ b/other/install_3-0_PostgreSQL.sql @@ -1210,7 +1210,7 @@ CREATE TABLE {$db_prefix}messages ( body text NOT NULL, icon varchar(16) NOT NULL DEFAULT 'xx', approved smallint NOT NULL DEFAULT '1', - reacts smallint NOT NULL DEFAULT '0', + reactions smallint NOT NULL DEFAULT '0', PRIMARY KEY (id_msg) ); @@ -1226,7 +1226,7 @@ CREATE INDEX {$db_prefix}messages_show_posts ON {$db_prefix}messages (id_member, CREATE INDEX {$db_prefix}messages_id_member_msg ON {$db_prefix}messages (id_member, approved, id_msg); CREATE INDEX {$db_prefix}messages_current_topic ON {$db_prefix}messages (id_topic, id_msg, id_member, approved); CREATE INDEX {$db_prefix}messages_related_ip ON {$db_prefix}messages (id_member, poster_ip, id_msg); -CREATE INDEX {$db_prefix}messages_reacts ON {$db_prefix}messages (reacts); +CREATE INDEX {$db_prefix}messages_reactions ON {$db_prefix}messages (reactions); # # Table structure for table `moderators` # diff --git a/other/upgrade_3-0_MySQL.sql b/other/upgrade_3-0_MySQL.sql index 177061c7e6..77f6a5162c 100644 --- a/other/upgrade_3-0_MySQL.sql +++ b/other/upgrade_3-0_MySQL.sql @@ -119,14 +119,16 @@ if(!in_array($cols, 'reactions')) upgrade_query("ALTER TABLE {db_prefix}user_reacts RENAME idx_liker TO idx_reactor"); // Rename the likes column in the messages table + upgrade_query("DROP INDEX idx_likes ON {db_prefix}messages"); Db::$db->change_column('{db_prefix}messages', 'likes', ['name' => 'reactions']); + upgrade_query("ADD INDEX idx_reactions ON {db_prefix}messages (reactions)"); // Update user alert prefs upgrade_query("UPDATE {db_prefix}user_alerts_prefs SET alert_pref='msg_react' WHERE alert_pref='msg_like'"); } else { - // Create the table + // Add the table upgrade_query(" CREATE TABLE {db_prefix}user_reacts ( id_member MEDIUMINT UNSIGNED DEFAULT '0', @@ -139,6 +141,10 @@ if(!in_array($cols, 'reactions')) INDEX idx_reactor (id_member) ) ENGINE=InnoDB "); + + // Add the reactions column and related index to the messages table + Db::$db->add_column('{db_prefix}messages', ['name' => 'reactions', 'type' => 'smallint', 'not_null' => true, 'default' => '0']); + Db::$db->add_index('{db_prefix}messages', ['name' => 'idx_messages_reactions', 'columns' => 'reactions']); } // Either way we want to add the new table diff --git a/other/upgrade_3-0_PostgreSQL.sql b/other/upgrade_3-0_PostgreSQL.sql index 06b2d71e2c..be2d6d6da1 100644 --- a/other/upgrade_3-0_PostgreSQL.sql +++ b/other/upgrade_3-0_PostgreSQL.sql @@ -119,13 +119,16 @@ if(!in_array($cols, 'reactions')) upgrade_query("ALTER INDEX idx_liker RENAME TO idx_reactor"); // Rename the likes column in the messages table + upgrade_query("DROP INDEX idx_messages_likes"); Db::$db->change_column('{db_prefix}messages', 'likes', ['name' => 'reactions']); + upgrade_query("CREATE INDEX idx_messages_reactions ON {db_prefix}messages (reactions)"); // Update user alert prefs upgrade_query("UPDATE {db_prefix}user_alerts_prefs SET alert_pref='msg_react' WHERE alert_pref='msg_like'"); } else { + // Add the table upgrade_query(" CREATE TABLE {db_prefix}user_reacts ( id_member mediumint default '0', @@ -138,6 +141,10 @@ if(!in_array($cols, 'reactions')) INDEX idx_reactor (id_member) ) "); + + // Add the reactions column and related index to the messages table + Db::$db->add_column('{db_prefix}messages', ['name' => 'reactions', 'type' => 'smallint', 'not_null' => true, 'default' => '0']); + Db::$db->add_index('{db_prefix}messages', ['name' => 'idx_messages_reactions', 'columns' => 'reactions']); } // Either way we want to add the new table From 9e85a058da30ff56cec53a370781888a5dc461bf Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sat, 4 May 2024 00:09:00 -0400 Subject: [PATCH 05/77] Update permission and setting... --- other/upgrade_3-0_MySQL.sql | 6 ++++++ other/upgrade_3-0_PostgreSQL.sql | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/other/upgrade_3-0_MySQL.sql b/other/upgrade_3-0_MySQL.sql index e801cd6827..3091ba7888 100644 --- a/other/upgrade_3-0_MySQL.sql +++ b/other/upgrade_3-0_MySQL.sql @@ -844,6 +844,12 @@ if(!in_array($cols, 'reactions')) // Update user alert prefs upgrade_query("UPDATE {db_prefix}user_alerts_prefs SET alert_pref='msg_react' WHERE alert_pref='msg_like'"); + + // Update permissions + upgrade_query("UPDATE {db_prefix}permissions SET permission='reacts_react' WHERE permission='likes_like'"); + + // And last but not least, the setting + upgrade_query("UPDATE {db_prefix}settings SET variable='enable_reacts' WHERE variable='enable_likes'"); } else { diff --git a/other/upgrade_3-0_PostgreSQL.sql b/other/upgrade_3-0_PostgreSQL.sql index 21d34ff6cf..d9545b3350 100644 --- a/other/upgrade_3-0_PostgreSQL.sql +++ b/other/upgrade_3-0_PostgreSQL.sql @@ -843,6 +843,15 @@ if(!in_array($cols, 'reactions')) // Update user alert prefs upgrade_query("UPDATE {db_prefix}user_alerts_prefs SET alert_pref='msg_react' WHERE alert_pref='msg_like'"); + + // Update user alert prefs + upgrade_query("UPDATE {db_prefix}user_alerts_prefs SET alert_pref='msg_react' WHERE alert_pref='msg_like'"); + + // Update permissions + upgrade_query("UPDATE {db_prefix}permissions SET permission='reacts_react' WHERE permission='likes_like'"); + + // And last but not least, the setting + upgrade_query("UPDATE {db_prefix}settings SET variable='enable_reacts' WHERE variable='enable_likes'"); } else { From 0b0b869493d8c0e46460bcb4e073ff7598d94c30 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sat, 4 May 2024 22:55:42 -0400 Subject: [PATCH 06/77] Rename Like.php and the Like class. Also adjusted another language string --- Languages/en_US/ManageSettings.php | 4 ++-- Sources/Actions/{Like.php => React.php} | 2 +- Sources/Forum.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename Sources/Actions/{Like.php => React.php} (99%) diff --git a/Languages/en_US/ManageSettings.php b/Languages/en_US/ManageSettings.php index 3ae32080a9..8a9ddb69bd 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/Sources/Actions/Like.php b/Sources/Actions/React.php similarity index 99% rename from Sources/Actions/Like.php rename to Sources/Actions/React.php index e983bb0c25..eda063dc8e 100644 --- a/Sources/Actions/Like.php +++ b/Sources/Actions/React.php @@ -31,7 +31,7 @@ /** * Handles liking posts and displaying the list of who liked a post. */ -class Like implements ActionInterface +class React implements ActionInterface { use ActionTrait; diff --git a/Sources/Forum.php b/Sources/Forum.php index 3f5aa64da4..464fb0d4b1 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'], From ac040faf4ede8134d4ab6ec7db4d0f1714912c37 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sat, 4 May 2024 23:46:25 -0400 Subject: [PATCH 07/77] More renaming. Also some other minor fixes --- Sources/Actions/Admin/ACP.php | 4 ++-- Sources/Actions/Admin/Features.php | 19 ++++++++++--------- Sources/Actions/Admin/Permissions.php | 12 ++++++------ other/upgrade_3-0_MySQL.sql | 2 +- other/upgrade_3-0_PostgreSQL.sql | 5 +---- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/Sources/Actions/Admin/ACP.php b/Sources/Actions/Admin/ACP.php index 69392dd19c..b59491c730 100644 --- a/Sources/Actions/Admin/ACP.php +++ b/Sources/Actions/Admin/ACP.php @@ -179,8 +179,8 @@ class ACP implements ActionInterface 'profile' => [ 'label' => 'custom_profile_shorttitle', ], - 'likes' => [ - 'label' => 'likes', + 'reactions' => [ + 'label' => 'reactions', ], 'mentions' => [ 'label' => 'mentions', diff --git a/Sources/Actions/Admin/Features.php b/Sources/Actions/Admin/Features.php index 45a9b7a4d5..ba0cbc43ab 100644 --- a/Sources/Actions/Admin/Features.php +++ b/Sources/Actions/Admin/Features.php @@ -73,7 +73,7 @@ class Features implements ActionInterface 'sig' => 'signature', 'profile' => 'profile', 'profileedit' => 'profileEdit', - 'likes' => 'likes', + 'reactions' => 'reactions', 'mentions' => 'mentions', 'alerts' => 'alerts', ]; @@ -1416,27 +1416,28 @@ public function profileEdit(): void } /** - * Handles modifying the likes settings. + * Handles modifying the reactions settings. * - * Accessed from ?action=admin;area=featuresettings;sa=likes + * Accessed from ?action=admin;area=featuresettings;sa=reactions + * @todo Add interface/code to manage available reactions */ - public function likes(): void + public function reactions(): void { - $config_vars = self::likesConfigVars(); + $config_vars = self::reactionsConfigVars(); // Saving? if (isset($_GET['save'])) { User::$me->checkSession(); - IntegrationHook::call('integrate_save_likes_settings'); + IntegrationHook::call('integrate_save_reactions_settings'); ACP::saveDBSettings($config_vars); $_SESSION['adm-save'] = true; - Utils::redirectexit('action=admin;area=featuresettings;sa=likes'); + Utils::redirectexit('action=admin;area=featuresettings;sa=reactions'); } - Utils::$context['post_url'] = Config::$scripturl . '?action=admin;area=featuresettings;save;sa=likes'; - Utils::$context['settings_title'] = Lang::$txt['likes']; + Utils::$context['post_url'] = Config::$scripturl . '?action=admin;area=featuresettings;save;sa=reactions'; + Utils::$context['settings_title'] = Lang::$txt['reactions']; ACP::prepareDBSettingContext($config_vars); } diff --git a/Sources/Actions/Admin/Permissions.php b/Sources/Actions/Admin/Permissions.php index 583efe5ca6..d16ed04d83 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, @@ -1669,9 +1669,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/other/upgrade_3-0_MySQL.sql b/other/upgrade_3-0_MySQL.sql index 3091ba7888..969e402f00 100644 --- a/other/upgrade_3-0_MySQL.sql +++ b/other/upgrade_3-0_MySQL.sql @@ -846,7 +846,7 @@ if(!in_array($cols, 'reactions')) upgrade_query("UPDATE {db_prefix}user_alerts_prefs SET alert_pref='msg_react' WHERE alert_pref='msg_like'"); // Update permissions - upgrade_query("UPDATE {db_prefix}permissions SET permission='reacts_react' WHERE permission='likes_like'"); + upgrade_query("UPDATE {db_prefix}permissions SET permission='reactions_react' WHERE permission='likes_like'"); // And last but not least, the setting upgrade_query("UPDATE {db_prefix}settings SET variable='enable_reacts' WHERE variable='enable_likes'"); diff --git a/other/upgrade_3-0_PostgreSQL.sql b/other/upgrade_3-0_PostgreSQL.sql index d9545b3350..4639461ae5 100644 --- a/other/upgrade_3-0_PostgreSQL.sql +++ b/other/upgrade_3-0_PostgreSQL.sql @@ -844,11 +844,8 @@ if(!in_array($cols, 'reactions')) // Update user alert prefs upgrade_query("UPDATE {db_prefix}user_alerts_prefs SET alert_pref='msg_react' WHERE alert_pref='msg_like'"); - // Update user alert prefs - upgrade_query("UPDATE {db_prefix}user_alerts_prefs SET alert_pref='msg_react' WHERE alert_pref='msg_like'"); - // Update permissions - upgrade_query("UPDATE {db_prefix}permissions SET permission='reacts_react' WHERE permission='likes_like'"); + upgrade_query("UPDATE {db_prefix}permissions SET permission='reactions_react' WHERE permission='likes_like'"); // And last but not least, the setting upgrade_query("UPDATE {db_prefix}settings SET variable='enable_reacts' WHERE variable='enable_likes'"); From 54a02fd82a6a6e2f75be1386a21e90bbda0bedf1 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sat, 4 May 2024 23:48:42 -0400 Subject: [PATCH 08/77] Missed something --- Sources/Actions/Admin/Features.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Actions/Admin/Features.php b/Sources/Actions/Admin/Features.php index ba0cbc43ab..5c3255482e 100644 --- a/Sources/Actions/Admin/Features.php +++ b/Sources/Actions/Admin/Features.php @@ -1702,18 +1702,18 @@ public static function sigConfigVars(): array } /** - * Gets the configuration variables for the likes sub-action. + * Gets the configuration variables for the reactions sub-action. * - * @return array $config_vars for the likes sub-action. + * @return array $config_vars for the reactions sub-action. */ - public static function likesConfigVars(): array + public static function reactionsConfigVars(): array { $config_vars = [ - ['check', 'enable_likes'], - ['permissions', 'likes_like'], + ['check', 'enable_reactions'], + ['permissions', 'reactions_react'], ]; - IntegrationHook::call('integrate_likes_settings', [&$config_vars]); + IntegrationHook::call('integrate_reactions_settings', [&$config_vars]); return $config_vars; } From 4a3289f50926deb093180ae42f7e1b62f07a8f08 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sat, 18 May 2024 09:59:46 -0400 Subject: [PATCH 09/77] Add support for changing the order of available reactions --- other/install_3-0_MySQL.sql | 1 + other/install_3-0_PostgreSQL.sql | 1 + other/upgrade_3-0_MySQL.sql | 3 ++- other/upgrade_3-0_PostgreSQL.sql | 3 ++- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/other/install_3-0_MySQL.sql b/other/install_3-0_MySQL.sql index 45bd5d1778..1726e64c0e 100644 --- a/other/install_3-0_MySQL.sql +++ b/other/install_3-0_MySQL.sql @@ -997,6 +997,7 @@ CREATE TABLE {$db_prefix}qanda ( CREATE TABLE {$db_prefix}reactions ( id_reaction SMALLINT UNSIGNED AUTO_INCREMENT, name VARCHAR(255) NOT NULL DEFAULT '', + order SMALLINT UNSIGNED NOT NULL DEFAULT '0', PRIMARY KEY (id_reaction) ) ENGINE={$engine}; diff --git a/other/install_3-0_PostgreSQL.sql b/other/install_3-0_PostgreSQL.sql index 09fde82fce..a7c312e0d0 100644 --- a/other/install_3-0_PostgreSQL.sql +++ b/other/install_3-0_PostgreSQL.sql @@ -1466,6 +1466,7 @@ CREATE SEQUENCE {$db_prefix}reactions_seq START WITH 0; CREATE TABLE {$db_prefix}reactions ( id_reaction smallint DEFAULT nextval('{$db_prefix}reactions_seq'), name varchar(255) NOT NULL DEFAULT '', + order smallint NOT NULL DEFAULT '0', PRIMARY KEY (id_reaction) ); diff --git a/other/upgrade_3-0_MySQL.sql b/other/upgrade_3-0_MySQL.sql index ed518831c9..815af1510e 100644 --- a/other/upgrade_3-0_MySQL.sql +++ b/other/upgrade_3-0_MySQL.sql @@ -1037,7 +1037,8 @@ if(!in_array($cols, 'reactions')) upgrade_query(" CREATE TABLE {db_prefix}reactions ( id_reaction SMALLINT UNSIGNED DEFAULT '0' AUTO_INCREMENT, - name varchar(255) NOT NULL DEFAULT '' + name varchar(255) NOT NULL DEFAULT '', + order SMALLINT UNSIGNED NOT NULL DEFAULT '0', ) "); // Default reaction is "like" diff --git a/other/upgrade_3-0_PostgreSQL.sql b/other/upgrade_3-0_PostgreSQL.sql index 01e9914386..a244b980ea 100644 --- a/other/upgrade_3-0_PostgreSQL.sql +++ b/other/upgrade_3-0_PostgreSQL.sql @@ -919,7 +919,8 @@ if(!in_array($cols, 'reactions')) upgrade_query(" CREATE TABLE {db_prefix}reactions ( id_reaction smallint default nextval('{db_prefix}reactions_seq'), - name varchar(255) not null default '' + name varchar(255) not null default '', + order smallint default '0', ) "); // Default reaction is "like" From bd372a283ab3683f193b27177244ec4c93bab1a0 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sat, 18 May 2024 12:15:55 -0400 Subject: [PATCH 10/77] Change getLikedMsgs to getReactedMsgs and update the logic slightly --- Sources/Actions/Display.php | 10 +++++----- Sources/Topic.php | 34 +++++++++++++++++----------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Sources/Actions/Display.php b/Sources/Actions/Display.php index ed3d111fcd..926cdc1e58 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/Topic.php b/Sources/Topic.php index ecb9ccf112..f10c2476b4 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; } /** @@ -1462,9 +1462,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(); } /****************** From 7f1ab57d8cfcbca66fb2caf1d58343392d6d5f26 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sat, 18 May 2024 12:48:58 -0400 Subject: [PATCH 11/77] Change a few more things... --- Sources/Msg.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/Msg.php b/Sources/Msg.php index 8cb99f68a0..cba7703bf6 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 @@ -468,15 +468,15 @@ 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 ($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']); } } From 0c3932754bec090d337bcccba12132c88ef7ac2c Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Mon, 27 May 2024 14:26:05 -0400 Subject: [PATCH 12/77] Fix a typo and update the stats info. --- Sources/Actions/Admin/Features.php | 2 +- Sources/Actions/Stats.php | 84 +++++++++++++++--------------- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/Sources/Actions/Admin/Features.php b/Sources/Actions/Admin/Features.php index f279ad7a18..17bbaa2a2b 100644 --- a/Sources/Actions/Admin/Features.php +++ b/Sources/Actions/Admin/Features.php @@ -1709,7 +1709,7 @@ public static function sigConfigVars(): array public static function reactionsConfigVars(): array { $config_vars = [ - ['check', 'enable_reactions'], + ['check', 'enable_reacts'], ['permissions', 'reactions_react'], ]; diff --git a/Sources/Actions/Stats.php b/Sources/Actions/Stats.php index dd31a557e9..b927322b42 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); } } From 09c5379ad3327fc1bdb6bf30b64c503279d3ca17 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Mon, 27 May 2024 14:28:48 -0400 Subject: [PATCH 13/77] Language file tweak for stats. --- Languages/en_US/Stats.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Languages/en_US/Stats.php b/Languages/en_US/Stats.php index f594768a1b..93a1b1296f 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'; From 7cea89fc470970f46c6ff879ba55de30584947c9 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sat, 1 Jun 2024 00:07:10 -0400 Subject: [PATCH 14/77] Rename the template and a bunch of other stuff... --- Sources/Actions/React.php | 216 +++++++++--------- ...Likes.template.php => Reacts.template.php} | 4 +- 2 files changed, 112 insertions(+), 108 deletions(-) rename Themes/default/{Likes.template.php => Reacts.template.php} (97%) diff --git a/Sources/Actions/React.php b/Sources/Actions/React.php index 6af68cf5e5..5ee077df3f 100644 --- a/Sources/Actions/React.php +++ b/Sources/Actions/React.php @@ -45,7 +45,7 @@ class React 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 +62,7 @@ class React 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 +117,14 @@ class React 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 +132,15 @@ class React 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 +153,8 @@ class React 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' => '', @@ -264,8 +264,8 @@ 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; @@ -278,14 +278,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; } @@ -336,48 +336,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_like = 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; @@ -394,8 +394,8 @@ 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']; + 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 +408,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 +427,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 +444,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 +456,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 +487,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 +499,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 +520,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 +559,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 +591,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_likes, ], ); } // 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 +711,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 +754,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 +776,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/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 14f786ab14..0e4450ea2e 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 '
    '; - if (!empty(Utils::$context['data']['can_like'])) + if (!empty(Utils::$context['data']['can_react'])) echo '
  • ', Utils::$context['data']['already_liked'] ? Lang::$txt['unlike'] : Lang::$txt['like'], ' From b49770a32d2c6a914c04208ea33cf2bfd26da532 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sun, 2 Jun 2024 18:16:59 -0400 Subject: [PATCH 15/77] Create a new trait for loading reactions. --- Sources/Actions/React.php | 8 ++++++ Sources/ReactionTrait.php | 55 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 Sources/ReactionTrait.php diff --git a/Sources/Actions/React.php b/Sources/Actions/React.php index 5ee077df3f..07e7a64aa3 100644 --- a/Sources/Actions/React.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; @@ -186,6 +187,13 @@ class React implements ActionInterface */ protected mixed $data; + /** + * @var array + * + * Array of valid reactions - name and ID + */ + protected array $racts = []; + /**************** * Public methods ****************/ diff --git a/Sources/ReactionTrait.php b/Sources/ReactionTrait.php new file mode 100644 index 0000000000..d40846ebc9 --- /dev/null +++ b/Sources/ReactionTrait.php @@ -0,0 +1,55 @@ +query( + '', + 'SELECT * FROM {db_prefix}reactions', + []); + + while ($result = Db::$db->fetchAssoc($request)) { + $this->reactions[$result['id_react']] = $result; + } + + Db::$db->free($request); + + // Cache the results + CacheApi::put('reactions', $reactions); + } + return $this->reactions; + } +} \ No newline at end of file From 0cbfb60c5bd9f6442b1136ef23481bbac4b5017a Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sun, 2 Jun 2024 18:19:16 -0400 Subject: [PATCH 16/77] This is handled by the trait so don't need it now... --- Sources/Actions/React.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Sources/Actions/React.php b/Sources/Actions/React.php index 07e7a64aa3..6bbe838727 100644 --- a/Sources/Actions/React.php +++ b/Sources/Actions/React.php @@ -187,13 +187,6 @@ class React implements ActionInterface */ protected mixed $data; - /** - * @var array - * - * Array of valid reactions - name and ID - */ - protected array $racts = []; - /**************** * Public methods ****************/ From 5ab211962a65decbcf511014291a7cdc89061b09 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Thu, 6 Jun 2024 10:26:05 -0400 Subject: [PATCH 17/77] A couple of minor tweaks... Signed-off-by: Michael Eshom --- Sources/Actions/Admin/Features.php | 6 +++--- Sources/Actions/React.php | 9 ++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Sources/Actions/Admin/Features.php b/Sources/Actions/Admin/Features.php index 17bbaa2a2b..62e1f640d9 100644 --- a/Sources/Actions/Admin/Features.php +++ b/Sources/Actions/Admin/Features.php @@ -1890,14 +1890,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/React.php b/Sources/Actions/React.php index 6bbe838727..4e35f8aa48 100644 --- a/Sources/Actions/React.php +++ b/Sources/Actions/React.php @@ -187,6 +187,13 @@ class React 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; + /**************** * Public methods ****************/ @@ -596,7 +603,7 @@ protected function react(): void WHERE id_msg = {int:id_msg}', [ 'id_msg' => $this->content, - 'num_reacts' => $this->num_likes, + 'num_reacts' => $this->num_reacts, ], ); } From 5cb6c5d04dea7b5a12594ec85693ed962a01cdb4 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sat, 8 Jun 2024 01:20:53 -0400 Subject: [PATCH 18/77] Add the initial file for the admin stuff and modify the admin menu --- Sources/Actions/Admin/ACP.php | 10 +++-- Sources/Actions/Admin/Reactions.php | 62 +++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 Sources/Actions/Admin/Reactions.php diff --git a/Sources/Actions/Admin/ACP.php b/Sources/Actions/Admin/ACP.php index 7b198dd32d..4ac273259b 100644 --- a/Sources/Actions/Admin/ACP.php +++ b/Sources/Actions/Admin/ACP.php @@ -179,9 +179,6 @@ class ACP implements ActionInterface 'profile' => [ 'label' => 'custom_profile_shorttitle', ], - 'reactions' => [ - 'label' => 'reactions', - ], 'mentions' => [ 'label' => 'mentions', ], @@ -365,6 +362,13 @@ class ACP implements ActionInterface ], ], ], + 'managereactions' => [ + 'label' => 'reactions_manage', + 'function' => __NAMESPACE__ . '\\Reactions::call', + 'icon' => 'like', + 'permission' => ['manage_reactions'], + 'subsections' => [], + ], 'manageattachments' => [ 'label' => 'attachments_avatars', 'function' => __NAMESPACE__ . '\\Attachments::call', diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php new file mode 100644 index 0000000000..c12d383266 --- /dev/null +++ b/Sources/Actions/Admin/Reactions.php @@ -0,0 +1,62 @@ + Date: Sun, 9 Jun 2024 21:15:47 -0400 Subject: [PATCH 19/77] Move the settings to the new file and add initial code for handling updates/deletions/additions --- Sources/Actions/Admin/Features.php | 17 --- Sources/Actions/Admin/Find.php | 1 + Sources/Actions/Admin/Reactions.php | 183 +++++++++++++++++++++++++++- 3 files changed, 182 insertions(+), 19 deletions(-) diff --git a/Sources/Actions/Admin/Features.php b/Sources/Actions/Admin/Features.php index 62e1f640d9..647837e434 100644 --- a/Sources/Actions/Admin/Features.php +++ b/Sources/Actions/Admin/Features.php @@ -1701,23 +1701,6 @@ public static function sigConfigVars(): array return $config_vars; } - /** - * Gets the configuration variables for the reactions sub-action. - * - * @return array $config_vars for the reactions sub-action. - */ - public static function reactionsConfigVars(): array - { - $config_vars = [ - ['check', 'enable_reacts'], - ['permissions', 'reactions_react'], - ]; - - IntegrationHook::call('integrate_reactions_settings', [&$config_vars]); - - return $config_vars; - } - /** * Gets the configuration variables for the mentions sub-action. * diff --git a/Sources/Actions/Admin/Find.php b/Sources/Actions/Admin/Find.php index 05ec9731a3..6608045d36 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/Reactions.php b/Sources/Actions/Admin/Reactions.php index c12d383266..9ba1967b6b 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -29,19 +29,198 @@ use SMF\Lang; use SMF\Logging; use SMF\Menu; +use SMF\SecurityToken; use SMF\Theme; use SMF\User; use SMF\Utils; +/** + * This class handles everything related to managing reactions. + */ class Reactions implements ActionInterface { + use ActionTrait; + use ReactionTrait; + + /******************* + * Public properties + *******************/ + /** - * @inheritDoc + * @var string + * Our default subaction + */ + public string $subaction = 'settings'; + + + /************************** + * Public static properties + **************************/ + + /** @var array + * Available subactions + */ + public static $subactions = [ + 'edit' => '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'; + + // Finish up the form... + Utils::$context['post_url'] = Config::$scripturl . '?action=admin;area=reactions;save;sa=settings'; + } + + /*********************** + * 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 { - // TODO: Implement execute() method. + $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. We'll use this even if we're not updating anything + $reactions = $this->getReactions(); + + // They must have submitted a form. + if (isset($_POST['react_save'])) { + User::$me->checkSession(); + SecurityToken::validate('admin-mss', 'request'); + + // This will indicate whether we need to update the reactions cache later... + $do_update = false; + + // Anything to delete? + if (isset($_POST['delete_reacts'])) { + $do_update = true; + + foreach($_POST['delete_reacts'] as $to_delete) { + $deleted[] = (int) $to_delete; + } + + // Now to do the actual deleting + Db::$db->query(' + DELETE FROM {db_pref}reactions + WHERE id_react 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_pref}reactions + GROUP BY id_msg + WHERE id_react IN ({array_int:deleted})', + [ + '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? + if (isset($_POST['reacts'])) { + // Adding things? + if (isset($_POST['react_add'])) { + foreach($_POST['react_add'] as $new_react) + { + if (!empty($new_react)) { + $add[] = $new_react; + } + } + + if (!empty($add)) { + $do_update = true; + Db::$db->insert('{db_pref}reactions', $add); + } + } + + // Updating things... + $updates = []; + foreach($_POST['reacts'] as $id => $name) { + // Did they update this one? + if ($reactions[$id] != $name) { + $updates[$id] = $name; + } + } + + // Anything to update? + if (!empty($updates)) { + $do_update = true; + // Do the update + Db::$db->insert('replace', ['id_react, name'], $updates, 'id_react'); + } + + // If we updated anything, re-cache everything + if ($do_update) { + } + } + } + + // Load up the info for the form... } /** From 82cbe71d2569b90897c04686c3ca9249978162bd Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sun, 9 Jun 2024 21:38:55 -0400 Subject: [PATCH 20/77] Minor tweaks and such --- Sources/Actions/Admin/Reactions.php | 10 ++++++++-- Sources/ReactionTrait.php | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 9ba1967b6b..da99391973 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -17,6 +17,7 @@ namespace SMF\Actions\Admin; +use Overtrue\PHPLint\Cache; use SMF\ActionInterface; use SMF\ActionTrait; use SMF\ReactionTrait; @@ -125,7 +126,7 @@ public function editreactions(): void // Make sure we select the right menu item Menu::$loaded['admin']['currentsubsection'] = 'editreactions'; - // Get the reactions. We'll use this even if we're not updating anything + // Get the reactions. If we're updating things then we'll overwrite this later $reactions = $this->getReactions(); // They must have submitted a form. @@ -194,7 +195,9 @@ public function editreactions(): void if (!empty($add)) { $do_update = true; - Db::$db->insert('{db_pref}reactions', $add); + + // Insert the new reactions + Db::$db->insert('insert', '{db_pref}reactions', ['name'], $add, []); } } @@ -216,6 +219,9 @@ public function editreactions(): void // If we updated anything, re-cache everything if ($do_update) { + // Re-cache the reactions and update the reactions variable + Cache::put('reactions', null); + $reactions = $this->getReactions(); } } } diff --git a/Sources/ReactionTrait.php b/Sources/ReactionTrait.php index d40846ebc9..e776eaef53 100644 --- a/Sources/ReactionTrait.php +++ b/Sources/ReactionTrait.php @@ -42,13 +42,13 @@ public function getReactions(): array []); while ($result = Db::$db->fetchAssoc($request)) { - $this->reactions[$result['id_react']] = $result; + $this->reactions[$result['id_react']] = $result['name']; } Db::$db->free($request); // Cache the results - CacheApi::put('reactions', $reactions); + CacheApi::putData('reactions', $reactions); } return $this->reactions; } From 51c8f6ab6298332796f722d078c97b795f47bd2d Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sun, 9 Jun 2024 21:42:49 -0400 Subject: [PATCH 21/77] Fix the typo, add a comment block and remove the unneeded trait property --- Sources/ReactionTrait.php | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Sources/ReactionTrait.php b/Sources/ReactionTrait.php index e776eaef53..4804c4c0d6 100644 --- a/Sources/ReactionTrait.php +++ b/Sources/ReactionTrait.php @@ -18,16 +18,11 @@ Use SMF\Cache\CacheApi; Use SMF\Db\DatabaseApi as Db; +/** + * This trait only has one purpose - define a method to load reactions so we don't have to duplicate code + */ trait ReactionTrait { - - /** - * @var array - * - * An array of information about available reactions - */ - protected array $reactions = []; - /** * @return array * @@ -48,8 +43,8 @@ public function getReactions(): array Db::$db->free($request); // Cache the results - CacheApi::putData('reactions', $reactions); + CacheApi::put('reactions', $reactions); } - return $this->reactions; + return $reactions; } } \ No newline at end of file From b38c849abe620d0a6c743c0bac2ea5ea6671fc04 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Fri, 14 Jun 2024 00:35:48 -0400 Subject: [PATCH 22/77] Code to handle adding/removing/updating reactions and initial code for the list. More to come... --- Sources/Actions/Admin/Reactions.php | 60 +++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index da99391973..19a388d2bf 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -226,22 +226,52 @@ public function editreactions(): void } } - // Load up the info for the form... - } + // Set up the form now... + + // Create our token + SecurityToken::create('admin-mr', 'request'); + + // 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=reactions;sa=edit', + 'get_items' => [ + 'function' => function() use ($reactions) : array { + $items = []; + foreach ($reactions as $id => $name) { + // Make a nice text field... + $items[] = ''; + } + return $items; + }, + ], + 'get_count' => [ + 'value' => count($reactions), + ], + 'columns' => + [ + 'name' => + [ + 'header' => [ + 'value' => Lang::$txt['reactions_name'], + ] + ] - /** - * @inheritDoc - */ - public static function load(): static - { - // TODO: Implement load() method. - } + ] + ]; - /** - * @inheritDoc - */ - public static function call(): void - { - // TODO: Implement call() method. + // Now that we have our list options set up, have some fun... + $listOptions['form'] = [ + 'href' => Config::$scripturl . '?action=admin;area=react;sa=edit;' . Utils::$context['session_var'] . '=' . Utils::$context['session_id'], + 'name' => 'list_reactions', + ]; + + new ItemList($listOptions); + + Utils::$context['page_title'] = Lang::$txt['reactions_manage']; + Utils::$context['sub_template'] = 'show_list'; + Utils::$context['default_list'] = 'list_reactions'; } } \ No newline at end of file From 29929857785444c9efa4ba72b6c0fa7ba503b6d0 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Fri, 14 Jun 2024 00:37:35 -0400 Subject: [PATCH 23/77] HTML5, not XHTML... --- Sources/Actions/Admin/Reactions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 19a388d2bf..1e3fbdb61d 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -242,7 +242,7 @@ public function editreactions(): void $items = []; foreach ($reactions as $id => $name) { // Make a nice text field... - $items[] = ''; + $items[] = ''; } return $items; }, From 2b510042a1c52f9724c5386aed6b1df227570427 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sun, 16 Jun 2024 21:56:49 -0400 Subject: [PATCH 24/77] More random fixes and such --- Sources/Actions/Admin/Reactions.php | 33 ++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 1e3fbdb61d..c59934410c 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -17,7 +17,6 @@ namespace SMF\Actions\Admin; -use Overtrue\PHPLint\Cache; use SMF\ActionInterface; use SMF\ActionTrait; use SMF\ReactionTrait; @@ -62,7 +61,7 @@ class Reactions implements ActionInterface /** @var array * Available subactions */ - public static $subactions = [ + public static array $subactions = [ 'edit' => 'editreactions', 'settings' => 'settings', ]; @@ -140,6 +139,7 @@ public function editreactions(): void // Anything to delete? if (isset($_POST['delete_reacts'])) { $do_update = true; + $deleted = []; foreach($_POST['delete_reacts'] as $to_delete) { $deleted[] = (int) $to_delete; @@ -188,6 +188,8 @@ public function editreactions(): void if (isset($_POST['react_add'])) { foreach($_POST['react_add'] as $new_react) { + // No funny stuff now.. + $new_react = trim($new_react); if (!empty($new_react)) { $add[] = $new_react; } @@ -204,8 +206,11 @@ public function editreactions(): void // Updating things... $updates = []; foreach($_POST['reacts'] as $id => $name) { - // Did they update this one? - if ($reactions[$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; } } @@ -220,7 +225,7 @@ public function editreactions(): void // If we updated anything, re-cache everything if ($do_update) { // Re-cache the reactions and update the reactions variable - Cache::put('reactions', null); + CacheApi::put('reactions', null); $reactions = $this->getReactions(); } } @@ -262,6 +267,24 @@ public function editreactions(): void ] ]; + // The column for deleting things + $listOptions['columns']['remove'] = [ + 'header' => [ + 'value' => Lang::$txt['reacts_delete'], + 'style' => 'width:3%', + ], + 'data' => [ + 'function' => function () use ($reactions) { + $checks = []; + foreach(array_keys($reactions) as $id) { + $checks[] = ''; + } + return $checks; + }, + ], + 'class' => 'centertext', + ]; + // Now that we have our list options set up, have some fun... $listOptions['form'] = [ 'href' => Config::$scripturl . '?action=admin;area=react;sa=edit;' . Utils::$context['session_var'] . '=' . Utils::$context['session_id'], From 5b3cbc17893afcc202683b0a22b23b4d76408472 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Fri, 21 Jun 2024 22:18:56 -0400 Subject: [PATCH 25/77] A few more minor tweaks: - Add code to handle separate buttons for deleting/saving - Add placeholders for additional row and inline JS - Add closing PHP tag for consistency --- Sources/Actions/Admin/Reactions.php | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index c59934410c..ebc656192f 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -129,7 +129,7 @@ public function editreactions(): void $reactions = $this->getReactions(); // They must have submitted a form. - if (isset($_POST['react_save'])) { + if (isset($_POST['react_save']) || isset($_POST['react_delete'])) { User::$me->checkSession(); SecurityToken::validate('admin-mss', 'request'); @@ -137,7 +137,7 @@ public function editreactions(): void $do_update = false; // Anything to delete? - if (isset($_POST['delete_reacts'])) { + if (isset($_POST['react_delete']) && isset($_POST['delete_reacts'])) { $do_update = true; $deleted = []; @@ -181,9 +181,8 @@ public function editreactions(): void } } } - // Updating things? - if (isset($_POST['reacts'])) { + elseif (isset($_POST['reacts'])) { // Adding things? if (isset($_POST['react_add'])) { foreach($_POST['react_add'] as $new_react) @@ -285,6 +284,16 @@ public function editreactions(): void 'class' => 'centertext', ]; + // Add a row for a blank field to add a reaction, and a link to add another blank field. + $listOptions['additional_rows'][] = [ + + ]; + + // And some inline JS to handle adding another row + $listOptions['inline_javascript'] = [ + + ]; + // Now that we have our list options set up, have some fun... $listOptions['form'] = [ 'href' => Config::$scripturl . '?action=admin;area=react;sa=edit;' . Utils::$context['session_var'] . '=' . Utils::$context['session_id'], @@ -297,4 +306,5 @@ public function editreactions(): void Utils::$context['sub_template'] = 'show_list'; Utils::$context['default_list'] = 'list_reactions'; } -} \ No newline at end of file +} +?> \ No newline at end of file From 968de8a951513d552c5f5e87f8cdf51c406ee320 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sat, 22 Jun 2024 00:11:09 -0400 Subject: [PATCH 26/77] More fun... - Update the get_items function per specs - Move the do_update conditional to the proper spot - Add the buttons and a checkbox column for deleting stuff --- Sources/Actions/Admin/Reactions.php | 50 ++++++++++++++++++++++------- Sources/ReactionTrait.php | 4 +-- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index ebc656192f..c892a6607f 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -220,13 +220,14 @@ public function editreactions(): void // Do the update Db::$db->insert('replace', ['id_react, name'], $updates, 'id_react'); } + } - // If we updated anything, re-cache everything - if ($do_update) { - // Re-cache the reactions and update the reactions variable - CacheApi::put('reactions', null); - $reactions = $this->getReactions(); - } + // 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); } } @@ -242,11 +243,13 @@ public function editreactions(): void 'no_items_label' => Lang::$txt['no_reactions'], 'base_href' => Config::$scripturl . '?action=admin;area=reactions;sa=edit', 'get_items' => [ - 'function' => function() use ($reactions) : array { + 'function' => function(int $start, int $items_per_page, string $sort_by, array $params) use ($reactions) : array { $items = []; foreach ($reactions as $id => $name) { - // Make a nice text field... - $items[] = ''; + $items[] = [ + 'name' => '', + 'check' => '', + ]; } return $items; }, @@ -286,12 +289,35 @@ public function editreactions(): void // Add a row for a blank field to add a reaction, and a link to add another blank field. $listOptions['additional_rows'][] = [ - + [ + 'position' => 'bottom_of_list', + 'data' => [ + 'value' => '' + ] + ], + [ + // Clicking this magic link adds a new row... + 'position' => 'bottom_of_list', + 'data' => [ + 'value' => '' . Lang::$txt['reacts_add'] . '' + ] + ], + [ + // And last but not least our buttons + 'position' => 'below_table_data', + 'data' => [ + 'value' => ' + ', + 'class' => 'centercol', + ], ] - ] ]; @@ -299,7 +304,7 @@ public function editreactions(): void // Clicking this magic link adds a new row... 'position' => 'bottom_of_list', 'data' => [ - 'value' => '' . Lang::$txt['reacts_add'] . '' + 'value' => '' ], [ // And last but not least our input buttons From 4d1e38756fd41b6ecb531f4f4c2de12eb3222481 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Jul 2024 11:38:21 -0400 Subject: [PATCH 55/77] A couple more typos --- Sources/Actions/Admin/Reactions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 974cb87978..66cdf34a28 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -256,7 +256,7 @@ public function editreactions(): void 'id' => 'reactions_list', 'title' => Lang::$txt['reactions'], 'no_items_label' => Lang::$txt['no_reactions'], - 'base_href' => Config::$scripturl . '?action=admin;area=reactions;sa=edit', + '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 = []; @@ -330,7 +330,7 @@ function addrow() { // Now that we have our list options set up, have some fun... $listOptions['form'] = [ - 'href' => Config::$scripturl . '?action=admin;area=react;sa=edit;' . Utils::$context['session_var'] . '=' . Utils::$context['session_id'], + 'href' => Config::$scripturl . '?action=admin;area=managereactions;sa=edit;' . Utils::$context['session_var'] . '=' . Utils::$context['session_id'], 'name' => 'list_reactions', ]; From 24dfc97e92b8baf907b94d1a8e27d06f20a65200 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Jul 2024 11:43:04 -0400 Subject: [PATCH 56/77] Make our add button look nice --- Sources/Actions/Admin/Reactions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 66cdf34a28..099033b37b 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -309,7 +309,7 @@ public function editreactions(): void [ // Clicking this magic button adds a new row... 'position' => 'bottom_of_list', - 'value' => '' + 'value' => '' ], [ // And last but not least our input buttons From 2eecf3507ca9ffcf6fcf22fc3be0e5752cf9b725 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Jul 2024 11:48:28 -0400 Subject: [PATCH 57/77] Some more tweaks/fixes --- Sources/Actions/Admin/Reactions.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 099033b37b..08c124c781 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -146,7 +146,7 @@ public function editreactions(): void $reactions = $this->getReactions(); // They must have submitted a form. - if (isset($_POST['react_save']) || isset($_POST['react_delete'])) { + if (isset($_POST['reacts_save']) || isset($_POST['reacts_delete'])) { User::$me->checkSession(); SecurityToken::validate('admin-mss', 'request'); @@ -154,7 +154,7 @@ public function editreactions(): void $do_update = false; // Anything to delete? - if (isset($_POST['react_delete']) && isset($_POST['delete_reacts'])) { + if (isset($_POST['reacts_delete']) && isset($_POST['delete_reacts'])) { $do_update = true; $deleted = []; @@ -200,8 +200,8 @@ public function editreactions(): void } // Updating things? elseif (isset($_POST['reacts'])) { // Adding things? - if (isset($_POST['react_add'])) { - foreach ($_POST['react_add'] as $new_react) { + 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)) { @@ -303,12 +303,12 @@ public function editreactions(): void // Add a row for a blank field to add a reaction, and a link to add another blank field. $listOptions['additional_rows'] = [ [ - 'position' => 'bottom_of_list', + 'position' => 'below_table_data', 'value' => '' ], [ // Clicking this magic button adds a new row... - 'position' => 'bottom_of_list', + 'position' => 'below_table_data', 'value' => '' ], [ From 9129e268810b51b641f2a756192bf993638dc6a6 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Jul 2024 11:50:33 -0400 Subject: [PATCH 58/77] Another typo --- Sources/Actions/Admin/Reactions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 08c124c781..88c59e3287 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -148,7 +148,7 @@ public function editreactions(): void // They must have submitted a form. if (isset($_POST['reacts_save']) || isset($_POST['reacts_delete'])) { User::$me->checkSession(); - SecurityToken::validate('admin-mss', 'request'); + SecurityToken::validate('admin-mr', 'request'); // This will indicate whether we need to update the reactions cache later... $do_update = false; From f2c1a2b4259d5e326f91b347bccadacd0e8d5b30 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 3 Jul 2024 00:45:39 -0400 Subject: [PATCH 59/77] Validate token while saving settings --- Sources/Actions/Admin/Reactions.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 88c59e3287..884210e401 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -82,6 +82,7 @@ public static function settings(): void if (isset($_REQUEST['save'])) { User::$me->checkSession(); + SecurityToken::validate('admin-mr'); IntegrationHook::call('integrate_save_reactions_settings'); // Yeppers, saving this... @@ -148,7 +149,7 @@ public function editreactions(): void // They must have submitted a form. if (isset($_POST['reacts_save']) || isset($_POST['reacts_delete'])) { User::$me->checkSession(); - SecurityToken::validate('admin-mr', 'request'); + SecurityToken::validate('admin-mr'); // This will indicate whether we need to update the reactions cache later... $do_update = false; @@ -249,7 +250,7 @@ public function editreactions(): void // Set up the form now... // Create our token - SecurityToken::create('admin-mr', 'request'); + SecurityToken::create('admin-mr'); // Set up our list. Use a special function for the get_items so we can output things in input fields... $listOptions = [ From 4712347614fb8f954527f4a5ac250e86df811eab Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 3 Jul 2024 01:17:42 -0400 Subject: [PATCH 60/77] Add a title to the settings page and a token to the form on the manage page --- Sources/Actions/Admin/Reactions.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 884210e401..62f1d11e23 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -79,6 +79,7 @@ public static function settings(): void // 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(); @@ -333,6 +334,7 @@ function addrow() { $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); From a512c6aa14debaa89f37d50f9ee76304689d138f Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 3 Jul 2024 01:53:02 -0400 Subject: [PATCH 61/77] Typo --- Sources/Actions/Admin/Reactions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 62f1d11e23..68d48020b3 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -251,7 +251,7 @@ public function editreactions(): void // Set up the form now... // Create our token - SecurityToken::create('admin-mr'); + 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 = [ From 0e993f497870a9b294f38783cbc146aafbe99833 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Thu, 4 Jul 2024 13:49:32 -0400 Subject: [PATCH 62/77] Fix admin menu stuff. --- Sources/Actions/Admin/ACP.php | 3351 +++++++++++++++++---------------- 1 file changed, 1680 insertions(+), 1671 deletions(-) diff --git a/Sources/Actions/Admin/ACP.php b/Sources/Actions/Admin/ACP.php index 74fb7bd3bd..56e8d514ca 100644 --- a/Sources/Actions/Admin/ACP.php +++ b/Sources/Actions/Admin/ACP.php @@ -1,2002 +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', - ], - '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', - ], - 'reset' => [ - 'label' => 'themeadmin_reset_title', + 'languages' => [ + 'label' => 'language_configuration', + 'function' => __NAMESPACE__ . '\\Languages::call', + 'icon' => 'languages', + 'subsections' => [ + 'edit' => [ + 'label' => 'language_edit', + ], + 'add' => [ + 'label' => 'language_add', + ], + 'settings' => [ + 'label' => 'language_settings', + ], ], - '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', + ], ], ], - ], - 'managereactions' => [ - 'label' => 'reactions_manage', - 'function' => __NAMESPACE__ . '\\Reactions::call', - 'icon' => 'like', - 'permission' => ['admin_forum'], - 'subsections' => [], - ], - '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 From ff792fd56e9447a61f5d92b85fcfab0a82ef0d61 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Thu, 4 Jul 2024 14:05:05 -0400 Subject: [PATCH 63/77] Token name typo --- Sources/Actions/Admin/Reactions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 68d48020b3..03da8d6c79 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -150,7 +150,7 @@ public function editreactions(): void // They must have submitted a form. if (isset($_POST['reacts_save']) || isset($_POST['reacts_delete'])) { User::$me->checkSession(); - SecurityToken::validate('admin-mr'); + SecurityToken::validate('admin-mre'); // This will indicate whether we need to update the reactions cache later... $do_update = false; From f5c1854ced8670daccfe36c099f333f02966865a Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Thu, 4 Jul 2024 14:29:18 -0400 Subject: [PATCH 64/77] Add a key to the insert because the code has a meltdown otherwise... --- Sources/Actions/Admin/Reactions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 03da8d6c79..4c43d1e47d 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -215,7 +215,7 @@ public function editreactions(): void $do_update = true; // Insert the new reactions - Db::$db->insert('insert', '{db_pref}reactions', ['name'], $add, []); + Db::$db->insert('', '{db_pref}reactions', ['name'], $add, ['id_reaction']); } } From 5f4929ce624250bc7bb57f421d7d04c14f4237a8 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Thu, 4 Jul 2024 14:43:07 -0400 Subject: [PATCH 65/77] Fix the queries --- Sources/Actions/Admin/Reactions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 4c43d1e47d..fe557924e0 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -215,7 +215,7 @@ public function editreactions(): void $do_update = true; // Insert the new reactions - Db::$db->insert('', '{db_pref}reactions', ['name'], $add, ['id_reaction']); + Db::$db->insert('', '{db_prefix}reactions', ['name'], $add, ['id_reaction']); } } @@ -235,7 +235,7 @@ public function editreactions(): void if (!empty($updates)) { $do_update = true; // Do the update - Db::$db->insert('replace', ['id_react, name'], $updates, 'id_react'); + Db::$db->insert('replace', '{db_prefix}reactions', ['id_react, name'], $updates, 'id_react'); } } From 2dd635e3441ffba2c41747f8a6db31cb5710a643 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Fri, 5 Jul 2024 01:14:38 -0400 Subject: [PATCH 66/77] Fix the queries the right way this time --- Sources/Actions/Admin/Reactions.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index fe557924e0..a1023d2971 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -165,7 +165,7 @@ public function editreactions(): void } // Now to do the actual deleting - Db::$db->query(' + Db::$db->query('', ' DELETE FROM {db_pref}reactions WHERE id_react IN ({array_int:deleted})', [ @@ -174,7 +174,7 @@ public function editreactions(): void ); // Are there any posts that used these reactions? - $get_reacted_posts = Db::$db->query(' + $get_reacted_posts = Db::$db->query('', ' SELECT id_msg, COUNT (id_react) AS num_reacts FROM {db_pref}reactions GROUP BY id_msg @@ -188,7 +188,7 @@ public function editreactions(): void // Did we find anything? if (Db::$db->num_rows($get_reacted_posts) > 0) { while ($reacted_post = $get_reacted_posts->fetchAssoc()) { - Db::$db->query(' + Db::$db->query('', ' UPDATE {db_prefix}messages SET reactions = reactions-{int:deleted} WHERE id_msg = {int:msg}', @@ -207,7 +207,7 @@ public function editreactions(): void // No funny stuff now.. $new_react = trim($new_react); if (!empty($new_react)) { - $add[] = $new_react; + $add[] = [0 => $new_react]; } } @@ -215,7 +215,7 @@ public function editreactions(): void $do_update = true; // Insert the new reactions - Db::$db->insert('', '{db_prefix}reactions', ['name'], $add, ['id_reaction']); + Db::$db->insert('', '{db_prefix}reactions', ['name'], $add, []); } } From ee7e01f70a2e20629c389368fda7ac5c569bc58f Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Fri, 5 Jul 2024 01:23:34 -0400 Subject: [PATCH 67/77] Another typo --- Sources/Actions/Admin/Reactions.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index a1023d2971..106c7df662 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -161,13 +161,13 @@ public function editreactions(): void $deleted = []; foreach ($_POST['delete_reacts'] as $to_delete) { - $deleted[] = (int)$to_delete; + $deleted[] = (int) $to_delete; } // Now to do the actual deleting Db::$db->query('', ' DELETE FROM {db_pref}reactions - WHERE id_react IN ({array_int:deleted})', + WHERE id_reaction IN ({array_int:deleted})', [ 'deleted' => $deleted, ] @@ -178,7 +178,7 @@ public function editreactions(): void SELECT id_msg, COUNT (id_react) AS num_reacts FROM {db_pref}reactions GROUP BY id_msg - WHERE id_react IN ({array_int:deleted})', + WHERE id_reaction IN ({array_int:deleted})', [ 'deleted' => $deleted, ] From 64bb71dd39ff1fe394b7294b029dd202ce217894 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Fri, 5 Jul 2024 01:29:41 -0400 Subject: [PATCH 68/77] Missed those somehow... --- Sources/Actions/Admin/Reactions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 106c7df662..ee9f7d37a2 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -166,7 +166,7 @@ public function editreactions(): void // Now to do the actual deleting Db::$db->query('', ' - DELETE FROM {db_pref}reactions + DELETE FROM {db_prefix}reactions WHERE id_reaction IN ({array_int:deleted})', [ 'deleted' => $deleted, @@ -176,7 +176,7 @@ public function editreactions(): void // 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_pref}reactions + FROM {db_prefix}reactions GROUP BY id_msg WHERE id_reaction IN ({array_int:deleted})', [ From 83dd3d443ee3431530ec3c5815ec1a64d16cc012 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Fri, 5 Jul 2024 01:32:28 -0400 Subject: [PATCH 69/77] GROUP BY goes after WHERE, not before --- Sources/Actions/Admin/Reactions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index ee9f7d37a2..95596116df 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -177,8 +177,8 @@ public function editreactions(): void $get_reacted_posts = Db::$db->query('', ' SELECT id_msg, COUNT (id_react) AS num_reacts FROM {db_prefix}reactions - GROUP BY id_msg - WHERE id_reaction IN ({array_int:deleted})', + WHERE id_reaction IN ({array_int:deleted}) + GROUP BY id_msg', [ 'deleted' => $deleted, ] From bddc4927110d82b8901546999882efbfe7051a78 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Fri, 5 Jul 2024 01:34:22 -0400 Subject: [PATCH 70/77] Too many typos... --- Sources/Actions/Admin/Reactions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 95596116df..18c067b942 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -235,7 +235,7 @@ public function editreactions(): void if (!empty($updates)) { $do_update = true; // Do the update - Db::$db->insert('replace', '{db_prefix}reactions', ['id_react, name'], $updates, 'id_react'); + Db::$db->insert('replace', '{db_prefix}reactions', ['id_react, name'], $updates, ['id_reaction']); } } From 2cb628aa2e1d750c16020c9a02373ddf2d64bebd Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Fri, 5 Jul 2024 01:35:48 -0400 Subject: [PATCH 71/77] Fix our updates array --- Sources/Actions/Admin/Reactions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 18c067b942..2b7ed9a8ba 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -227,7 +227,7 @@ public function editreactions(): void // Did they update this one? Ignore empty ones for now if ($reactions[$id] != $name && !empty($name)) { - $updates[$id] = $name; + $updates[] = [$id, $name]; } } From 2c2c26b5b4e4b31d074074812b9ae609864f400a Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Fri, 5 Jul 2024 01:52:23 -0400 Subject: [PATCH 72/77] Properly fix the update this time. --- Sources/Actions/Admin/Reactions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 2b7ed9a8ba..38b1e2e59e 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -207,7 +207,7 @@ public function editreactions(): void // No funny stuff now.. $new_react = trim($new_react); if (!empty($new_react)) { - $add[] = [0 => $new_react]; + $add[] = [$new_react]; } } @@ -235,7 +235,7 @@ public function editreactions(): void if (!empty($updates)) { $do_update = true; // Do the update - Db::$db->insert('replace', '{db_prefix}reactions', ['id_react, name'], $updates, ['id_reaction']); + Db::$db->insert('replace', '{db_prefix}reactions', ['id_reaction' => 'int', 'name' => 'string'], $updates, ['id_reaction']); } } From b5d397f37b7945a05fb4ef1f73fd863f65902343 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Fri, 5 Jul 2024 01:54:48 -0400 Subject: [PATCH 73/77] Final fixes. Everything will work now. --- Sources/Actions/Admin/Reactions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php index 38b1e2e59e..ddb06ac7ba 100644 --- a/Sources/Actions/Admin/Reactions.php +++ b/Sources/Actions/Admin/Reactions.php @@ -215,7 +215,7 @@ public function editreactions(): void $do_update = true; // Insert the new reactions - Db::$db->insert('', '{db_prefix}reactions', ['name'], $add, []); + Db::$db->insert('', '{db_prefix}reactions', ['name' => 'string'], $add, []); } } From b7f949195dd5890036b632bc9eefdd4d687b783b Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sun, 4 Aug 2024 21:20:20 -0400 Subject: [PATCH 74/77] Add code to load reaction information for posts --- Sources/Msg.php | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/Sources/Msg.php b/Sources/Msg.php index 4c80c1f5d8..4c1cc0f8ac 100644 --- a/Sources/Msg.php +++ b/Sources/Msg.php @@ -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? @@ -473,7 +481,30 @@ public function format(int $counter = 0, array $format_options = []): array $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['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, From a3757cf2a78fa625327fead849730c33bf518cf4 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sat, 17 Aug 2024 01:09:55 -0400 Subject: [PATCH 75/77] Likes -> reacts... --- Sources/User.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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, From 3f5ad54d098ce7a8a9fe2a92d5242af33368fbb7 Mon Sep 17 00:00:00 2001 From: Michael Eshom Date: Sat, 17 Aug 2024 01:30:38 -0400 Subject: [PATCH 76/77] Delete reactions when a topic is deleted --- Sources/Topic.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Sources/Topic.php b/Sources/Topic.php index 3d70dc1dd2..ff8a31a397 100644 --- a/Sources/Topic.php +++ b/Sources/Topic.php @@ -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 From 64394fed358ec3a469ff607eb92ecb801f783dd5 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 18 Aug 2024 19:41:40 -0400 Subject: [PATCH 77/77] Make sure the reaction ID passed in the URL is valid --- Languages/en_US/General.php | 1 + Sources/Actions/React.php | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Languages/en_US/General.php b/Languages/en_US/General.php index 19f0cbcac4..6265471d2a 100644 --- a/Languages/en_US/General.php +++ b/Languages/en_US/General.php @@ -834,6 +834,7 @@ 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/Sources/Actions/React.php b/Sources/Actions/React.php index da56eca85a..64a04109bc 100644 --- a/Sources/Actions/React.php +++ b/Sources/Actions/React.php @@ -35,6 +35,7 @@ class React implements ActionInterface { use ActionTrait; + use ReactionTrait; /******************* * Public properties @@ -164,7 +165,7 @@ class React implements ActionInterface ]; /** - * @var int + * @var int * * The topic ID. Used for liking messages. */ @@ -189,11 +190,16 @@ class React implements ActionInterface /** * @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 ****************/ @@ -276,6 +282,7 @@ protected function __construct() $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) { @@ -309,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. @@ -367,7 +381,7 @@ protected function check(): void * * See also issueReact() for further notes. */ - $can_like = IntegrationHook::call('integrate_valid_reacts', [$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; @@ -400,8 +414,8 @@ protected function check(): void } } - // Is the user able to like this? - // Viewing a list of likes doesn't require this permission. + // 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'];