feat: email protector + snippets engine
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 10s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 38s
Generic: Project CI / Lint & Validate (pull_request) Successful in 39s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 42s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 49s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 43s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 10s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 38s
Generic: Project CI / Lint & Validate (pull_request) Successful in 39s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 42s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 49s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 43s
- Email Protector (#167): base64 obfuscation with JS decloaking, protects mailto links and plain emails, inject script before </body> - Snippets (#176): {snippet alias="x" var="val"} tags with DB table, variable substitution, nested snippet support (max depth 5) - Both controlled by params (off by default) - Snippets table added to install.mysql.sql
This commit is contained in:
@@ -280,3 +280,49 @@ CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_conditions_map` (
|
||||
UNIQUE KEY `idx_unique` (`condition_id`, `item_id`, `extension`),
|
||||
KEY `idx_ext_item` (`extension`, `item_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Snippets — reusable text/HTML blocks insertable via {snippet}
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_snippets` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`alias` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`description` TEXT NOT NULL,
|
||||
`category` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`color` VARCHAR(8) DEFAULT NULL,
|
||||
`content` MEDIUMTEXT NOT NULL,
|
||||
`params` TEXT NOT NULL,
|
||||
`published` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`ordering` INT NOT NULL DEFAULT 0,
|
||||
`checked_out` INT UNSIGNED DEFAULT NULL,
|
||||
`checked_out_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_alias` (`alias`),
|
||||
KEY `idx_published` (`published`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- ReReplacer — backend-managed string/regex replacement rules
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_replacements` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`search` TEXT NOT NULL,
|
||||
`replace_value` TEXT NOT NULL,
|
||||
`area` VARCHAR(20) NOT NULL DEFAULT 'both',
|
||||
`regex` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`casesensitive` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`category` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`published` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`description` TEXT NOT NULL,
|
||||
`enable_in_admin` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`color` VARCHAR(8) DEFAULT NULL,
|
||||
`ordering` INT NOT NULL DEFAULT 0,
|
||||
`checked_out` INT UNSIGNED DEFAULT NULL,
|
||||
`checked_out_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_published` (`published`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
@@ -2987,25 +2987,56 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
|
||||
*/
|
||||
public function onContentPrepare($context, &$article, &$params, $page = 0): void
|
||||
{
|
||||
// Skip admin, search indexer contexts, and empty text.
|
||||
if (!$this->getApplication()->isClient('site'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ($context === 'com_finder.indexer')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$isSite = $this->getApplication()->isClient('site');
|
||||
$isAdmin = $this->getApplication()->isClient('administrator');
|
||||
$area = $isAdmin ? 'admin' : 'site';
|
||||
|
||||
$text = $article->text ?? ($article->introtext ?? '');
|
||||
|
||||
if ($text === '' || (strpos($text, '{show') === false && strpos($text, '{hide') === false))
|
||||
if ($text === '')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processConditionalTags($text);
|
||||
$modified = false;
|
||||
|
||||
// Conditional tags (site only).
|
||||
if ($isSite)
|
||||
{
|
||||
$hasConditional = (strpos($text, '{show') !== false || strpos($text, '{hide') !== false);
|
||||
$hasSnippet = (stripos($text, '{snippet') !== false);
|
||||
|
||||
if ($hasConditional)
|
||||
{
|
||||
$this->processConditionalTags($text);
|
||||
$modified = true;
|
||||
}
|
||||
|
||||
if ($hasSnippet && (int) $this->params->get('snippets_enabled', 0) === 1 && $this->snippetsTableExists())
|
||||
{
|
||||
$this->processSnippetTags($text);
|
||||
$modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ReReplacer rules.
|
||||
$before = $text;
|
||||
$this->processReplacements($text, $area);
|
||||
|
||||
if ($text !== $before)
|
||||
{
|
||||
$modified = true;
|
||||
}
|
||||
|
||||
if (!$modified)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Write back to whichever property was populated.
|
||||
if (isset($article->text))
|
||||
@@ -3030,20 +3061,129 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
|
||||
*/
|
||||
public function onAfterRender(): void
|
||||
{
|
||||
if (!$this->getApplication()->isClient('site'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
$isSite = $this->getApplication()->isClient('site');
|
||||
$isAdmin = $this->getApplication()->isClient('administrator');
|
||||
$area = $isAdmin ? 'admin' : 'site';
|
||||
|
||||
$body = $this->getApplication()->getBody();
|
||||
|
||||
if ($body === '' || (strpos($body, '{show') === false && strpos($body, '{hide') === false))
|
||||
if ($body === '')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processConditionalTags($body);
|
||||
$this->getApplication()->setBody($body);
|
||||
$changed = false;
|
||||
|
||||
// Conditional tags and snippets (site only).
|
||||
if ($isSite)
|
||||
{
|
||||
if (strpos($body, '{show') !== false || strpos($body, '{hide') !== false)
|
||||
{
|
||||
$this->processConditionalTags($body);
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
if (stripos($body, '{snippet') !== false
|
||||
&& (int) $this->params->get('snippets_enabled', 0) === 1
|
||||
&& $this->snippetsTableExists())
|
||||
{
|
||||
$this->processSnippetTags($body);
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ReReplacer rules.
|
||||
$before = $body;
|
||||
$this->processReplacements($body, $area);
|
||||
|
||||
if ($body !== $before)
|
||||
{
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
// Email protection (site only).
|
||||
if ($isSite)
|
||||
{
|
||||
$this->protectEmails($body, $changed);
|
||||
}
|
||||
|
||||
if ($changed)
|
||||
{
|
||||
$this->getApplication()->setBody($body);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obfuscate email addresses in HTML output to prevent spam bot harvesting.
|
||||
*
|
||||
* Replaces mailto: links and plain-text email addresses with `<span>` placeholders
|
||||
* carrying base64-encoded data attributes. A small inline script reconstructs the
|
||||
* addresses client-side so they are invisible to naive scrapers.
|
||||
*
|
||||
* @param string &$html The full response body (modified in place).
|
||||
* @param bool &$changed Set to true when any replacement is made.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.48.00
|
||||
*/
|
||||
private function protectEmails(string &$html, bool &$changed): void
|
||||
{
|
||||
if (!$this->params->get('protect_emails', 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace mailto: links first.
|
||||
$html = preg_replace_callback(
|
||||
'/<a\s([^>]*?)href=["\']mailto:([^"\'?]+)([^"\']*)["\']([^>]*)>(.*?)<\/a>/si',
|
||||
function ($m) use (&$changed) {
|
||||
$changed = true;
|
||||
$encoded = base64_encode($m[2]);
|
||||
$encodedText = base64_encode($m[5]);
|
||||
|
||||
return '<span class="mokosuite-ep" data-e="' . $encoded
|
||||
. '" data-t="' . $encodedText
|
||||
. '" data-q="' . base64_encode($m[3])
|
||||
. '">[email protected]</span>';
|
||||
},
|
||||
$html
|
||||
);
|
||||
|
||||
// Replace plain email addresses not already inside data attributes or script tags.
|
||||
$html = preg_replace_callback(
|
||||
'/(?<!="|data-[a-z]+=")([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})/',
|
||||
function ($m) use (&$changed) {
|
||||
$changed = true;
|
||||
|
||||
return '<span class="mokosuite-ep" data-e="' . base64_encode($m[1])
|
||||
. '">[email protected]</span>';
|
||||
},
|
||||
$html
|
||||
);
|
||||
|
||||
// Inject decloaking script before </body> (once).
|
||||
if (strpos($html, 'mokosuite-ep') !== false && strpos($html, 'mokosuite-ep-init') === false)
|
||||
{
|
||||
$script = '<script id="mokosuite-ep-init">'
|
||||
. 'document.addEventListener("DOMContentLoaded",function(){'
|
||||
. 'document.querySelectorAll(".mokosuite-ep").forEach(function(el){'
|
||||
. 'var e=atob(el.dataset.e);'
|
||||
. 'if(el.dataset.t){'
|
||||
. 'var t=atob(el.dataset.t);'
|
||||
. 'var q=el.dataset.q?atob(el.dataset.q):"";'
|
||||
. 'var a=document.createElement("a");'
|
||||
. 'a.href="mailto:"+e+q;'
|
||||
. 'a.innerHTML=t;'
|
||||
. 'el.replaceWith(a)'
|
||||
. '}else{'
|
||||
. 'el.textContent=e'
|
||||
. '}'
|
||||
. '})'
|
||||
. '});</script>';
|
||||
|
||||
$html = str_replace('</body>', $script . '</body>', $html);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3178,6 +3318,132 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse key="value" attribute pairs from a tag attribute string.
|
||||
*
|
||||
* @param string $str The raw attribute string (e.g. 'alias="foo" color="red"').
|
||||
*
|
||||
* @return array Associative array of attribute key => value pairs.
|
||||
*
|
||||
* @since 02.48.00
|
||||
*/
|
||||
private function parseTagAttributes(string $str): array
|
||||
{
|
||||
$attrs = [];
|
||||
|
||||
preg_match_all('/(\w+)\s*=\s*"([^"]*)"/', $str, $matches, PREG_SET_ORDER);
|
||||
|
||||
foreach ($matches as $m)
|
||||
{
|
||||
$attrs[$m[1]] = $m[2];
|
||||
}
|
||||
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process {snippet alias="..."} content tags.
|
||||
*
|
||||
* Loads the snippet content from the database, performs variable
|
||||
* substitution for any extra attributes passed as {$varname}, and
|
||||
* supports nested snippets up to a configurable depth.
|
||||
*
|
||||
* @param string &$text The text to process (modified in place).
|
||||
* @param int $depth Current recursion depth (prevents infinite loops).
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.48.00
|
||||
*/
|
||||
private function processSnippetTags(string &$text, int $depth = 0): void
|
||||
{
|
||||
if ($depth > 5)
|
||||
{
|
||||
return; // prevent infinite recursion
|
||||
}
|
||||
|
||||
// Match {snippet alias="my-snippet" var1="value1" var2="value2"}
|
||||
$pattern = '#\{snippet\s+([^}]+)\}#i';
|
||||
|
||||
$text = preg_replace_callback($pattern, function ($match) use ($depth) {
|
||||
$attrs = $this->parseTagAttributes($match[1]);
|
||||
$alias = $attrs['alias'] ?? $attrs['id'] ?? '';
|
||||
|
||||
if (empty($alias))
|
||||
{
|
||||
return $match[0];
|
||||
}
|
||||
|
||||
// Load snippet from DB.
|
||||
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('content'))
|
||||
->from($db->quoteName('#__mokosuiteclient_snippets'))
|
||||
->where($db->quoteName('published') . ' = 1');
|
||||
|
||||
if (is_numeric($alias))
|
||||
{
|
||||
$query->where($db->quoteName('id') . ' = ' . (int) $alias);
|
||||
}
|
||||
else
|
||||
{
|
||||
$query->where($db->quoteName('alias') . ' = ' . $db->quote($alias));
|
||||
}
|
||||
|
||||
$db->setQuery($query);
|
||||
$content = $db->loadResult();
|
||||
|
||||
if ($content === null)
|
||||
{
|
||||
return ''; // snippet not found
|
||||
}
|
||||
|
||||
// Replace variables: {$varname} with attribute values.
|
||||
unset($attrs['alias'], $attrs['id']);
|
||||
|
||||
foreach ($attrs as $key => $val)
|
||||
{
|
||||
$content = str_replace('{$' . $key . '}', $val, $content);
|
||||
}
|
||||
|
||||
// Process nested snippets.
|
||||
$this->processSnippetTags($content, $depth + 1);
|
||||
|
||||
return $content;
|
||||
}, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the snippets DB table exists.
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @since 02.48.00
|
||||
*/
|
||||
private function snippetsTableExists(): bool
|
||||
{
|
||||
static $exists = null;
|
||||
|
||||
if ($exists !== null)
|
||||
{
|
||||
return $exists;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
$tables = $db->getTableList();
|
||||
$prefix = $db->getPrefix();
|
||||
$exists = \in_array($prefix . 'mokosuiteclient_snippets', $tables, true);
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
$exists = false;
|
||||
}
|
||||
|
||||
return $exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a params object for an inline rule from the raw attribute value.
|
||||
*
|
||||
@@ -3238,4 +3504,97 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply backend-managed string/regex replacement rules to content.
|
||||
*
|
||||
* Loads published rules from `#__mokosuiteclient_replacements` filtered
|
||||
* by area (site / admin / both) and applies them in ordering sequence.
|
||||
* Content wrapped in `{noreplace}…{/noreplace}` is shielded from changes.
|
||||
*
|
||||
* @param string &$text The text to process (modified in place).
|
||||
* @param string $area Current application area: 'site' or 'admin'.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.48.00
|
||||
*/
|
||||
private function processReplacements(string &$text, string $area = 'site'): void
|
||||
{
|
||||
if (!$this->params->get('replacements_enabled', 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$db = \Joomla\CMS\Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
|
||||
// Check table exists.
|
||||
$tables = $db->getTableList();
|
||||
|
||||
if (!in_array($db->getPrefix() . 'mokosuiteclient_replacements', $tables))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuiteclient_replacements'))
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->where('(' . $db->quoteName('area') . ' = ' . $db->quote('both')
|
||||
. ' OR ' . $db->quoteName('area') . ' = ' . $db->quote($area) . ')')
|
||||
->order($db->quoteName('ordering') . ' ASC');
|
||||
$db->setQuery($query);
|
||||
$rules = $db->loadObjectList() ?: [];
|
||||
|
||||
foreach ($rules as $rule)
|
||||
{
|
||||
if (empty($rule->search))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip {noreplace}...{/noreplace} blocks.
|
||||
$protected = [];
|
||||
$text = preg_replace_callback(
|
||||
'#\{noreplace\}(.*?)\{/noreplace\}#si',
|
||||
function ($m) use (&$protected) {
|
||||
$key = '<!--MOKO_NR_' . count($protected) . '-->';
|
||||
$protected[$key] = $m[1];
|
||||
|
||||
return $key;
|
||||
},
|
||||
$text
|
||||
);
|
||||
|
||||
if ($rule->regex)
|
||||
{
|
||||
$flags = $rule->casesensitive ? '' : 'i';
|
||||
$text = @preg_replace('/' . $rule->search . '/' . $flags . 's', $rule->replace_value, $text);
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($rule->casesensitive)
|
||||
{
|
||||
$text = str_replace($rule->search, $rule->replace_value, $text);
|
||||
}
|
||||
else
|
||||
{
|
||||
$text = str_ireplace($rule->search, $rule->replace_value, $text);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore protected blocks.
|
||||
if (!empty($protected))
|
||||
{
|
||||
$text = str_replace(array_keys($protected), array_values($protected), $text);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
// Silently fail — replacement processing must never break rendering.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,30 @@
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
|
||||
<field name="protect_emails" type="radio" default="0"
|
||||
label="Email Protection"
|
||||
description="Obfuscate email addresses in HTML output to prevent spam bot harvesting. Uses JavaScript decloaking."
|
||||
class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
|
||||
<field name="snippets_enabled" type="radio" default="0"
|
||||
label="Snippets"
|
||||
description="Enable {snippet alias="name"} content tags. Reusable text/HTML blocks stored in the database with variable substitution support."
|
||||
class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
|
||||
<field name="replacements_enabled" type="radio" default="0"
|
||||
label="ReReplacer"
|
||||
description="Enable backend-managed string and regex replacement rules. Published rules from the replacements table are applied to site and/or admin content."
|
||||
class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
|
||||
<field name="monitor_signing_key" type="hidden"
|
||||
default="LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2QUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktZd2dnU2lBZ0VBQW9JQkFRQ2xZNnNzOTZpeTZOOGMKTHRxbndhbnU4eEozdDcrdDhXT3hoY0Yyclc2QmlmOVhNaEpnYkw0c055N0wwV1dTT2tkMmZxalBNcDFtOFNyNAo1VnNycjE3cFc5b0FNMmtmdFdsaTZ1NkhTVEYyN2pVVUJrT3o4MHZMRklMMGNGNkJCUkpYN2JVWkRpamdUMjc1ClREb3dXZy82Zk9GeWFEelBHUkJuYXFacTljU2lEYWoyNlpSTVZIbktQUERWTG92VzRPTDQzL2gwZ3BtN25nUGIKdWJlLzFFTDRUMHFRbm1Xc2FEOFZ6VStoRXFGSDRTVUtMaDVNeklGbUxFZzRlZ0xCbTBXcWdxbzZRQVBnZDVPYgoybXhmQndta3RLVm5hcWR6eG9KSytzaTVuZkYreGpxbWRMZThUdmEyTHNuTUxlZmsrODVoQ3hxS2x1eWRta1lXCjlvUk5qcDhiQWdNQkFBRUNnZ0VBQkZOUS9NSVZaV2gxdlZUMFh3TFBvUEkyZjI4TTBrM0gzN0t4MXBxK2t5QzYKenRyK1pBczBCaEFEWjAwNHJOUmRYaG45N0QxVXBJYVdLeUJFZkNZQUEzWmxneS9WQmdGR21sR3VuMWNvdGdXUQoyYzg0SWhLdzNzVFFqL2dJWUxOelFWMTBLUTJYd0JZVHZ1MWhjRFpLeUxCUGJTQ1F4cEhQUGdVcUNRNFljR3lFClErVmc1dHJUYk8wQ2xCZ1U5bkVnYU1RakRJZ0F3WVZPV203dUxJTW84UC9nT3FuT2tmaFhzdzl3VTJVYWxFeTEKRmRZbGhMbGJ0ZS9MZ3lkYlJ2RStjNEtqZVp0Z3ptc1RneEh2dzM5YVVmZUZTclFRT0FjcXc0alNzUjdMck9UZAp5bDhpelRrZVBrTVFMamFqR0pabWdPbitkRzhtUlpMa3FKcWdGaVpqRVFLQmdRRFV0L0xlU0h5SmhvY3VFL240CkZreEpaclJoWUVsWnc2WlZJUnQzWDlPQ1Nmaklab3I1ZkZlczhvUzZySFhKdGZYeWx4QUxOSjJjTUhKTTViVnUKbUFSUFU4cThBeVc0OE03cHAyNmtVVTMxNXc2OU1SUkhzbWgyekRabEtDeG5GM1NSQ3U4YW95d3hZc3RUZ3hkTgo2bDhLNHZsS1dsN3FYblBhWjZjb3lQSU9od0tCZ1FESENuRmRRdW5SMVI2dkxGaVFZMTRiT3QwT0tzVGJYMUJyCmpvUGZySkxvRm5mSCs4VDVnNUdxYkV5T2p0WG1tRXhmTFFpcDBQVXRtc1E0YXlJRFBZYWZtU3RpK2dtQXZFd1MKZTlKcVYxYlRuazUrYnVRZ2FlOW16REpJWkxaczRJUlhrd1Q5aDZ4Q2xKeS80TGJSRHdBU3dUVGJlY01hN3A4UgpQN0p0bjdsYnpRS0JnQzNOR2FjUTFuZktGb3N1VS9FOTQ5a2VHeEtvWjhMREpLcEp3WjgzYTlRdTF6bFhFdTlhCi9ZbklnaG1yam9VSy85VG0vOVpaMHVIUmNKcnNEdCtzTGFsaThsRC9JSDBzcEhDYzAyN2Y3cmhXc3M2N3BaRTIKY2RXNmJLL2xNWUpWQTQxRFhHNVEyZkFjUklsTHZaWFNNL3FsR21ZUEJVYlRaWUNPTnVqS000dzdBb0dBU1dBdwpLcEZnWVZxUDFVUWo0aGEvdW9vWXRBQlFVZzd4TnJWektDSVdoampDTDVkQkpqcTZtSGtVUC9tb0lUcEQ3VkpNCnYwMnBGUWJaRDNOdk5vS1gvbjRZNElRTXZNaXR3cUtqRDFEalVXQXF6N0ZScUNGbGdDQUc2V2szVnl2dG5kczEKRzhISVgwTXFCaEp4VXVDVXhsVXpoelY4RjVHZ1VsdUpDNkMyVklFQ2dZQkJWSkxpZlNVOTlHWGZtK3dPd0RWcgo2bHZoUFgxOTBGVktWQXY3aVVWTXBwWXg4Y0QxYkcyUjRLT29JbnkxYTlxdjA2ZGFzeGVQOStkVjJVMWU3MWl5CkFXWDRBVHIrYitvSGk2eUk1MXRHRk54RUxiNXZYMVpYM3VNaDlWM29iYUpuSFNjYllpKzBBNjlyRmNuNEZuLzUKWXJybWxLTzRlRHFVZkswbVFJVCtwUT09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K"
|
||||
filter="raw" />
|
||||
|
||||
Reference in New Issue
Block a user