diff --git a/UPGRADE_GUIDE.md b/UPGRADE_GUIDE.md
index 98bf8ca1c..78277f13e 100644
--- a/UPGRADE_GUIDE.md
+++ b/UPGRADE_GUIDE.md
@@ -5,6 +5,7 @@ This guide helps you upgrade your TorrentPier installation to the latest version
## 📖 Table of Contents
- [Configuration System Migration](#configuration-system-migration)
+- [Censor System Migration](#censor-system-migration)
- [Breaking Changes](#breaking-changes)
- [Best Practices](#best-practices)
@@ -85,6 +86,80 @@ if (isset(config()->bt_announce_url)) {
}
```
+## 🛡️ Censor System Migration
+
+The word censoring system has been refactored to use a singleton pattern, similar to the Configuration system, providing better performance and consistency.
+
+### Quick Migration Overview
+
+```php
+// ❌ Old way (still works, but not recommended)
+global $wordCensor;
+$censored = $wordCensor->censorString($text);
+
+// ✅ New way (recommended)
+$censored = censor()->censorString($text);
+```
+
+### Key Censor Changes
+
+#### Basic Usage
+```php
+// Censor a string
+$text = "This contains badword content";
+$censored = censor()->censorString($text);
+
+// Check if censoring is enabled
+if (censor()->isEnabled()) {
+ $censored = censor()->censorString($text);
+} else {
+ $censored = $text;
+}
+
+// Get count of loaded censored words
+$wordCount = censor()->getWordsCount();
+```
+
+#### Advanced Usage
+```php
+// Add runtime censored words (temporary, not saved to database)
+censor()->addWord('badword', '***');
+censor()->addWord('anotherbad*', 'replaced'); // Wildcards supported
+
+// Reload censored words from database (useful after admin updates)
+censor()->reload();
+
+// Check if censoring is enabled
+$isEnabled = censor()->isEnabled();
+```
+
+### Backward Compatibility
+
+The global `$wordCensor` variable is still available and works exactly as before:
+
+```php
+// This still works - backward compatibility maintained
+global $wordCensor;
+$censored = $wordCensor->censorString($text);
+
+// But this is now preferred
+$censored = censor()->censorString($text);
+```
+
+### Performance Benefits
+
+- **Single Instance**: Only one censor instance loads words from database
+- **Automatic Reloading**: Words are automatically reloaded when updated in admin panel
+- **Memory Efficient**: Shared instance across entire application
+- **Lazy Loading**: Words only loaded when censoring is enabled
+
+### Admin Panel Updates
+
+When you update censored words in the admin panel, the system now automatically:
+1. Updates the datastore cache
+2. Reloads the singleton instance with fresh words
+3. Applies changes immediately without requiring page refresh
+
## ⚠️ Breaking Changes
### Deprecated Functions
@@ -92,6 +167,10 @@ if (isset(config()->bt_announce_url)) {
- `set_config()` → Use `config()->set()`
- Direct `$bb_cfg` access → Use `config()` methods
+### Deprecated Patterns
+- `new TorrentPier\Censor()` → Use `censor()` global function
+- Direct `$wordCensor` access → Use `censor()` methods
+
### File Structure Changes
- New `/src/` directory for modern PHP classes
- Reorganized template structure
@@ -123,6 +202,31 @@ class TrackerService {
}
```
+### Censor Management
+```php
+// ✅ Check if censoring is enabled before processing
+function processUserInput(string $text): string {
+ if (censor()->isEnabled()) {
+ return censor()->censorString($text);
+ }
+ return $text;
+}
+
+// ✅ Use the singleton consistently
+class ForumPost {
+ public function getDisplayText(): string {
+ return censor()->censorString($this->text);
+ }
+}
+
+// ✅ Add runtime words when needed
+function setupCustomCensoring(): void {
+ if (isCustomModeEnabled()) {
+ censor()->addWord('custombad*', '[censored]');
+ }
+}
+```
+
### Error Handling
```php
// ✅ Graceful error handling
diff --git a/admin/admin_words.php b/admin/admin_words.php
index ba0746ca1..94f11caba 100644
--- a/admin/admin_words.php
+++ b/admin/admin_words.php
@@ -81,6 +81,7 @@ if ($mode != '') {
}
$datastore->update('censor');
+ censor()->reload(); // Reload the singleton instance with updated words
$message .= ' ' . sprintf($lang['CLICK_RETURN_WORDADMIN'], '', ' ') . ' ' . sprintf($lang['CLICK_RETURN_ADMIN_INDEX'], '', ' ');
bb_die($message);
@@ -95,6 +96,7 @@ if ($mode != '') {
}
$datastore->update('censor');
+ censor()->reload(); // Reload the singleton instance with updated words
bb_die($lang['WORD_REMOVED'] . ' ' . sprintf($lang['CLICK_RETURN_WORDADMIN'], '', ' ') . ' ' . sprintf($lang['CLICK_RETURN_ADMIN_INDEX'], '', ' '));
} else {
diff --git a/common.php b/common.php
index b896f4194..256dc8a3a 100644
--- a/common.php
+++ b/common.php
@@ -86,7 +86,8 @@ if (is_file(BB_PATH . '/library/config.local.php')) {
require_once BB_PATH . '/library/config.local.php';
}
-// Initialize Config singleton
+/** @noinspection PhpUndefinedVariableInspection */
+// Initialize Config singleton, bb_cfg from global file config
$config = \TorrentPier\Config::init($bb_cfg);
/**
@@ -99,6 +100,16 @@ function config(): \TorrentPier\Config
return \TorrentPier\Config::getInstance();
}
+/**
+ * Get the Censor instance
+ *
+ * @return \TorrentPier\Censor
+ */
+function censor(): \TorrentPier\Censor
+{
+ return \TorrentPier\Censor::getInstance();
+}
+
/**
* Initialize debug
*/
diff --git a/index.php b/index.php
index c946c758f..55d64e381 100644
--- a/index.php
+++ b/index.php
@@ -339,7 +339,7 @@ if (config()->get('show_latest_news')) {
$template->assign_block_vars('news', [
'NEWS_TOPIC_ID' => $news['topic_id'],
- 'NEWS_TITLE' => str_short($wordCensor->censorString($news['topic_title']), config()->get('max_news_title')),
+ 'NEWS_TITLE' => str_short(censor()->censorString($news['topic_title']), config()->get('max_news_title')),
'NEWS_TIME' => bb_date($news['topic_time'], 'd-M', false),
'NEWS_IS_NEW' => is_unread($news['topic_time'], $news['topic_id'], $news['forum_id']),
]);
@@ -362,7 +362,7 @@ if (config()->get('show_network_news')) {
$template->assign_block_vars('net', [
'NEWS_TOPIC_ID' => $net['topic_id'],
- 'NEWS_TITLE' => str_short($wordCensor->censorString($net['topic_title']), config()->get('max_net_title')),
+ 'NEWS_TITLE' => str_short(censor()->censorString($net['topic_title']), config()->get('max_net_title')),
'NEWS_TIME' => bb_date($net['topic_time'], 'd-M', false),
'NEWS_IS_NEW' => is_unread($net['topic_time'], $net['topic_id'], $net['forum_id']),
]);
diff --git a/library/ajax/posts.php b/library/ajax/posts.php
index 2c5405df3..2cff05d00 100644
--- a/library/ajax/posts.php
+++ b/library/ajax/posts.php
@@ -11,7 +11,7 @@ if (!defined('IN_AJAX')) {
die(basename(__FILE__));
}
-global $lang, $userdata, $wordCensor;
+global $lang, $userdata;
if (!isset($this->request['type'])) {
$this->ajax_die('empty type');
@@ -80,7 +80,7 @@ switch ($this->request['type']) {
// hide sid
$message = preg_replace('#(?<=[\?&;]sid=)[a-zA-Z0-9]#', 'sid', $message);
- $message = $wordCensor->censorString($message);
+ $message = censor()->censorString($message);
if ($post['post_id'] == $post['topic_first_post_id']) {
$message = "[quote]" . $post['topic_title'] . "[/quote]\r";
diff --git a/library/includes/bbcode.php b/library/includes/bbcode.php
index 7e5a40916..391814489 100644
--- a/library/includes/bbcode.php
+++ b/library/includes/bbcode.php
@@ -401,12 +401,12 @@ function add_search_words($post_id, $post_message, $topic_title = '', $only_retu
function bbcode2html($text)
{
- global $bbcode, $wordCensor;
+ global $bbcode;
if (!isset($bbcode)) {
$bbcode = new TorrentPier\Legacy\BBCode();
}
- $text = $wordCensor->censorString($text);
+ $text = censor()->censorString($text);
return $bbcode->bbcode2html($text);
}
diff --git a/library/includes/init_bb.php b/library/includes/init_bb.php
index 622e696cf..d1fd494f4 100644
--- a/library/includes/init_bb.php
+++ b/library/includes/init_bb.php
@@ -379,8 +379,10 @@ require_once INC_DIR . '/functions.php';
config()->merge(bb_get_config(BB_CONFIG));
$bb_cfg = config()->all();
+// wordCensor deprecated, but kept for compatibility with non-adapted code
+$wordCensor = censor();
+
$log_action = new TorrentPier\Legacy\LogAction();
-$wordCensor = new TorrentPier\Censor();
$html = new TorrentPier\Legacy\Common\Html();
$user = new TorrentPier\Legacy\Common\User();
diff --git a/library/includes/ucp/topic_watch.php b/library/includes/ucp/topic_watch.php
index c684cbabc..c75538a11 100644
--- a/library/includes/ucp/topic_watch.php
+++ b/library/includes/ucp/topic_watch.php
@@ -83,7 +83,7 @@ if ($watch_count > 0) {
'ROW_CLASS' => (!($i % 2)) ? 'row1' : 'row2',
'POST_ID' => $watch[$i]['topic_first_post_id'],
'TOPIC_ID' => $watch[$i]['topic_id'],
- 'TOPIC_TITLE' => str_short($wordCensor->censorString($watch[$i]['topic_title']), 70),
+ 'TOPIC_TITLE' => str_short(censor()->censorString($watch[$i]['topic_title']), 70),
'FULL_TOPIC_TITLE' => $watch[$i]['topic_title'],
'U_TOPIC' => TOPIC_URL . $watch[$i]['topic_id'],
'FORUM_TITLE' => $watch[$i]['forum_name'],
diff --git a/posting.php b/posting.php
index b41db5f6e..5fb5146cb 100644
--- a/posting.php
+++ b/posting.php
@@ -472,8 +472,8 @@ if ($refresh || $error_msg || ($submit && $topic_has_new_posts)) {
// hide sid
$message = preg_replace('#(?<=[\?&;]sid=)[a-zA-Z0-9]#', 'sid', $message);
- $subject = $wordCensor->censorString($subject);
- $message = $wordCensor->censorString($message);
+ $subject = censor()->censorString($subject);
+ $message = censor()->censorString($message);
if (!preg_match('/^Re:/', $subject) && !empty($subject)) {
$subject = 'Re: ' . $subject;
diff --git a/privmsg.php b/privmsg.php
index 42b47d1c1..409d0aacb 100644
--- a/privmsg.php
+++ b/privmsg.php
@@ -376,8 +376,8 @@ if ($mode == 'read') {
//
$post_subject = htmlCHR($privmsg['privmsgs_subject']);
$private_message = $privmsg['privmsgs_text'];
- $post_subject = $wordCensor->censorString($post_subject);
- $private_message = $wordCensor->censorString($private_message);
+ $post_subject = censor()->censorString($post_subject);
+ $private_message = censor()->censorString($private_message);
$private_message = bbcode2html($private_message);
//
@@ -1044,8 +1044,8 @@ if ($mode == 'read') {
if ($preview && !$error) {
$preview_message = bbcode2html($privmsg_message);
- $preview_subject = $wordCensor->censorString($privmsg_subject);
- $preview_message = $wordCensor->censorString($preview_message);
+ $preview_subject = censor()->censorString($privmsg_subject);
+ $preview_message = censor()->censorString($preview_message);
$s_hidden_fields = ' ';
$s_hidden_fields .= ' ';
@@ -1381,7 +1381,7 @@ if ($mode == 'read') {
$msg_userid = $row['user_id'];
$msg_user = profile_url($row);
- $msg_subject = $wordCensor->censorString($row['privmsgs_subject']);
+ $msg_subject = censor()->censorString($row['privmsgs_subject']);
$u_subject = PM_URL . "?folder=$folder&mode=read&" . POST_POST_URL . "=$privmsg_id";
diff --git a/search.php b/search.php
index b0d9bb977..01e977a65 100644
--- a/search.php
+++ b/search.php
@@ -571,7 +571,7 @@ if ($post_mode) {
'FORUM_ID' => $forum_id,
'FORUM_NAME' => $forum_name_html[$forum_id],
'TOPIC_ID' => $topic_id,
- 'TOPIC_TITLE' => $wordCensor->censorString($first_post['topic_title']),
+ 'TOPIC_TITLE' => censor()->censorString($first_post['topic_title']),
'TOPIC_ICON' => get_topic_icon($first_post, $is_unread_t),
));
@@ -586,7 +586,7 @@ if ($post_mode) {
}
$message = get_parsed_post($post);
- $message = $wordCensor->censorString($message);
+ $message = censor()->censorString($message);
$template->assign_block_vars('t.p', array(
'ROW_NUM' => $row_num,
@@ -787,7 +787,7 @@ else {
'FORUM_NAME' => $forum_name_html[$forum_id],
'TOPIC_ID' => $topic_id,
'HREF_TOPIC_ID' => $moved ? $topic['topic_moved_id'] : $topic['topic_id'],
- 'TOPIC_TITLE' => $wordCensor->censorString($topic['topic_title']),
+ 'TOPIC_TITLE' => censor()->censorString($topic['topic_title']),
'IS_UNREAD' => $is_unread,
'TOPIC_ICON' => get_topic_icon($topic, $is_unread),
'PAGINATION' => $moved ? '' : build_topic_pagination(TOPIC_URL . $topic_id, $topic['topic_replies'], config()->get('posts_per_page')),
diff --git a/src/Ajax.php b/src/Ajax.php
index a4005ee90..2debc0a20 100644
--- a/src/Ajax.php
+++ b/src/Ajax.php
@@ -68,7 +68,9 @@ class Ajax
*/
public function exec()
{
- global $lang;
+ /** @noinspection PhpUnusedLocalVariableInspection */
+ // bb_cfg deprecated, but kept for compatibility with non-adapted ajax files
+ global $bb_cfg, $lang;
// Exit if we already have errors
if (!empty($this->response['error_code'])) {
diff --git a/src/Censor.php b/src/Censor.php
index 5d564240e..a5e699998 100644
--- a/src/Censor.php
+++ b/src/Censor.php
@@ -10,11 +10,15 @@
namespace TorrentPier;
/**
- * Class Censor
- * @package TorrentPier
+ * Word Censoring System
+ *
+ * Singleton class that provides word censoring functionality
+ * with automatic loading of censored words from the datastore.
*/
class Censor
{
+ private static ?Censor $instance = null;
+
/**
* Word replacements
*
@@ -32,7 +36,34 @@ class Censor
/**
* Initialize word censor
*/
- public function __construct()
+ private function __construct()
+ {
+ $this->loadCensoredWords();
+ }
+
+ /**
+ * Get the singleton instance of Censor
+ */
+ public static function getInstance(): Censor
+ {
+ if (self::$instance === null) {
+ self::$instance = new self();
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Initialize the censor system (for compatibility)
+ */
+ public static function init(): Censor
+ {
+ return self::getInstance();
+ }
+
+ /**
+ * Load censored words from datastore
+ */
+ private function loadCensoredWords(): void
{
global $datastore;
@@ -59,4 +90,56 @@ class Censor
{
return preg_replace($this->words, $this->replacements, $word);
}
+
+ /**
+ * Reload censored words from datastore
+ * Useful when words are updated in admin panel
+ */
+ public function reload(): void
+ {
+ $this->words = [];
+ $this->replacements = [];
+ $this->loadCensoredWords();
+ }
+
+ /**
+ * Check if censoring is enabled
+ */
+ public function isEnabled(): bool
+ {
+ return config()->get('use_word_censor', false);
+ }
+
+ /**
+ * Add a censored word (runtime only)
+ *
+ * @param string $word
+ * @param string $replacement
+ */
+ public function addWord(string $word, string $replacement): void
+ {
+ $this->words[] = '#(?replacements[] = $replacement;
+ }
+
+ /**
+ * Get all censored words count
+ */
+ public function getWordsCount(): int
+ {
+ return count($this->words);
+ }
+
+ /**
+ * Prevent cloning of the singleton instance
+ */
+ private function __clone() {}
+
+ /**
+ * Prevent unserialization of the singleton instance
+ */
+ public function __wakeup()
+ {
+ throw new \Exception("Cannot unserialize a singleton.");
+ }
}
diff --git a/src/Legacy/Atom.php b/src/Legacy/Atom.php
index 090f23aee..5c50a7655 100644
--- a/src/Legacy/Atom.php
+++ b/src/Legacy/Atom.php
@@ -179,7 +179,7 @@ class Atom
*/
private static function create_atom($file_path, $mode, $id, $title, $topics)
{
- global $lang, $wordCensor;
+ global $lang;
$date = null;
$time = null;
$dir = \dirname($file_path);
@@ -213,7 +213,7 @@ class Atom
if (isset($topic['tor_status'])) {
$tor_status = " ({$lang['TOR_STATUS_NAME'][$topic['tor_status']]})";
}
- $topic_title = $wordCensor->censorString($topic['topic_title']);
+ $topic_title = censor()->censorString($topic['topic_title']);
$author_name = $topic['first_username'] ?: $lang['GUEST'];
$last_time = $topic['topic_last_post_time'];
if ($topic['topic_last_post_edit_time']) {
diff --git a/src/Legacy/Post.php b/src/Legacy/Post.php
index feb0a71f1..a3d933f20 100644
--- a/src/Legacy/Post.php
+++ b/src/Legacy/Post.php
@@ -341,7 +341,7 @@ class Post
*/
public static function user_notification($mode, &$post_data, &$topic_title, &$forum_id, &$topic_id, &$notify_user)
{
- global $lang, $userdata, $wordCensor;
+ global $lang, $userdata;
if (!config()->get('topic_notify_enabled')) {
return;
@@ -363,7 +363,7 @@ class Post
");
if ($watch_list) {
- $topic_title = $wordCensor->censorString($topic_title);
+ $topic_title = censor()->censorString($topic_title);
$u_topic = make_url(TOPIC_URL . $topic_id . '&view=newest#newest');
$unwatch_topic = make_url(TOPIC_URL . "$topic_id&unwatch=topic");
diff --git a/viewforum.php b/viewforum.php
index d1cfb460f..98519bce7 100644
--- a/viewforum.php
+++ b/viewforum.php
@@ -445,7 +445,7 @@ foreach ($topic_rowset as $topic) {
'FORUM_ID' => $forum_id,
'TOPIC_ID' => $topic_id,
'HREF_TOPIC_ID' => $moved ? $topic['topic_moved_id'] : $topic['topic_id'],
- 'TOPIC_TITLE' => $wordCensor->censorString($topic['topic_title']),
+ 'TOPIC_TITLE' => censor()->censorString($topic['topic_title']),
'TOPICS_SEPARATOR' => $separator,
'IS_UNREAD' => $is_unread,
'TOPIC_ICON' => get_topic_icon($topic, $is_unread),
diff --git a/viewtopic.php b/viewtopic.php
index b1117690d..efb89bc59 100644
--- a/viewtopic.php
+++ b/viewtopic.php
@@ -364,7 +364,7 @@ if (!$ranks = $datastore->get('ranks')) {
}
// Censor topic title
-$topic_title = $wordCensor->censorString($topic_title);
+$topic_title = censor()->censorString($topic_title);
// Post, reply and other URL generation for templating vars
$new_topic_url = POSTING_URL . "?mode=newtopic&" . POST_FORUM_URL . "=$forum_id";
@@ -624,8 +624,8 @@ for ($i = 0; $i < $total_posts; $i++) {
$user_sig = str_replace(
'\"', '"',
substr(
- preg_replace_callback('#(\>(((?>([^><]+|(?R)))*)\<))#s', function ($matches) use ($wordCensor) {
- return $wordCensor->censorString(reset($matches));
+ preg_replace_callback('#(\>(((?>([^><]+|(?R)))*)\<))#s', function ($matches) {
+ return censor()->censorString(reset($matches));
}, '>' . $user_sig . '<'), 1, -1
)
);
@@ -634,8 +634,8 @@ for ($i = 0; $i < $total_posts; $i++) {
$message = str_replace(
'\"', '"',
substr(
- preg_replace_callback('#(\>(((?>([^><]+|(?R)))*)\<))#s', function ($matches) use ($wordCensor) {
- return $wordCensor->censorString(reset($matches));
+ preg_replace_callback('#(\>(((?>([^><]+|(?R)))*)\<))#s', function ($matches) {
+ return censor()->censorString(reset($matches));
}, '>' . $message . '<'), 1, -1
)
);