PluginProbe
CryptX / 4.0.2
CryptX v4.0.2
4.2.0 4.1.1 trunk 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.9 2.0 2.1 2.2 2.3 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 All 92 releases
cryptx / classes / CryptX.php

CryptX.php in CryptX 4.0.2, at classes/CryptX.php

1,387 lines 49.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace CryptX;
4
5 final class CryptX
6 {
7
8 const NOT_FOUND = false;
9 const MAIL_IDENTIFIER = 'mailto:';
10 const SUBJECT_IDENTIFIER = "?subject=";
11 const INDEX_TO_CHECK = 4;
12 const PATTERN = '/(.*)(">)/i';
13 const ASCII_VALUES_BLACKLIST = ['32', '34', '39', '60', '62', '63', '92', '94', '96', '127'];
14 private static ?self $instance = null;
15 private static array $cryptXOptions = [];
16 private static int $imageCounter = 0;
17 private const FONT_EXTENSION = 'ttf';
18 private const PAYPAL_DONATION_URL = 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4026696';
19 private const MAILTO_PATTERN = '/<a (.*?)(href=("|\')mailto:(.*?)("|\')(.*?)|)>\s*(.*?)\s*<\/a>/i';
20 private const EMAIL_PATTERN = "/([_a-zA-Z0-9-+]+(\.[_a-zA-Z0-9-+]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(\.[a-zA-Z]{2,}))/i";
21 private CryptXSettingsTabs $settingsTabs;
22 private Config $config;
23
24 private function __construct()
25 {
26 $this->settingsTabs = new CryptXSettingsTabs($this);
27 $this->config = new Config(get_option('cryptX', []));
28 self::$cryptXOptions = $this->loadCryptXOptionsWithDefaults();
29 }
30
31 /**
32 * Retrieves the singleton instance of the class.
33 *
34 * @return self The singleton instance of the class.
35 */
36 public static function get_instance(): self
37 {
38 $needs_initialization = !(self::$instance instanceof self);
39
40 if ($needs_initialization) {
41 self::$instance = new self();
42 }
43
44 return self::$instance;
45 }
46
47
48 /**
49 * @return Config
50 */
51 public function getConfig(): Config
52 {
53 return $this->config;
54 }
55
56 /**
57 * Initializes the CryptX plugin by setting up version checks, applying filters, registering core hooks, initializing meta boxes (if enabled), and adding additional hooks.
58 *
59 * @return void
60 */
61 public function startCryptX(): void
62 {
63 $this->checkAndUpdateVersion();
64 $this->addUniversalWidgetFilters(); // Add this line
65 $this->initializePluginFilters();
66 $this->registerCoreHooks();
67 $this->initializeMetaBoxIfEnabled();
68 $this->registerAdditionalHooks();
69 }
70
71 /**
72 * Checks the current version of the application against the stored version and updates settings if the application version is newer.
73 *
74 * @return void
75 */
76 private function checkAndUpdateVersion(): void
77 {
78 $currentVersion = self::$cryptXOptions['version'] ?? null;
79 if ($currentVersion && version_compare(CRYPTX_VERSION, $currentVersion) > 0) {
80 $this->updateCryptXSettings();
81 }
82 }
83
84 /**
85 * Initializes and applies plugin filters based on the defined configuration options.
86 *
87 * @return void
88 */
89 public function initializePluginFilters(): void
90 {
91 if (empty($this->config)) {
92 return;
93 }
94
95 $activeFilters = $this->config->getActiveFilters();
96
97 foreach ($activeFilters as $filter) {
98 if ($filter === 'widget_text') {
99 $this->addWidgetFilters();
100 } else {
101 // Add autolink filters for non-widget filters if autolink is enabled
102 if ($this->config->isAutolinkEnabled()) {
103 $this->addAutoLinkFilters($filter, 11);
104 }
105 $this->addOtherFilters($filter);
106 }
107 }
108 }
109
110 /**
111 * Registers core hooks for the plugin's functionality.
112 *
113 * @return void
114 */
115 private function registerCoreHooks(): void
116 {
117 add_action('activate_' . CRYPTX_BASENAME, [$this, 'installCryptX']);
118 add_action('wp_enqueue_scripts', [$this, 'loadJavascriptFiles']);
119 }
120
121 /**
122 * Initializes the meta box functionality if enabled in the configuration.
123 *
124 * This method checks whether the meta box feature is enabled in the cryptX options.
125 * If enabled, it adds the necessary actions for administering the meta box and managing the posts' exclusion list.
126 *
127 * @return void
128 */
129 private function initializeMetaBoxIfEnabled(): void
130 {
131 if (!isset(self::$cryptXOptions['metaBox']) || !self::$cryptXOptions['metaBox']) {
132 return;
133 }
134
135 add_action('admin_menu', [$this, 'metaBox']);
136 add_action('wp_insert_post', [$this, 'addPostIdToExcludedList']);
137 add_action('wp_update_post', [$this, 'addPostIdToExcludedList']);
138 }
139
140 /**
141 * Registers additional WordPress hooks and shortcodes.
142 *
143 * @return void
144 */
145 private function registerAdditionalHooks(): void
146 {
147 add_filter('plugin_row_meta', [$this, 'add_plugin_action_links'], 10, 2);
148 add_filter('init', [$this, 'cryptXtinyUrl']);
149 add_shortcode('cryptx', [$this, 'cryptXShortcode']);
150 }
151
152 /**
153 * Retrieves the default options for CryptX configuration.
154 *
155 * @return array The default CryptX options, including version and font settings.
156 */
157 public function getCryptXOptionsDefaults(): array
158 {
159 return array_merge(
160 $this->config->getAll(),
161 [
162 'version' => CRYPTX_VERSION,
163 'c2i_font' => $this->getDefaultFont()
164 ]
165 );
166 }
167
168 /**
169 * Retrieves the default font from the available fonts directory.
170 *
171 * @return string|null Returns the name of the default font found, or null if no fonts are available.
172 */
173 private function getDefaultFont(): ?string
174 {
175 $availableFonts = $this->getFilesInDirectory(
176 CRYPTX_DIR_PATH . 'fonts',
177 [self::FONT_EXTENSION]
178 );
179
180 return $availableFonts[0] ?? null;
181 }
182
183 /**
184 * Loads the cryptX options with default values.
185 *
186 * @return array The cryptX options array with default values.
187 */
188 public function loadCryptXOptionsWithDefaults(): array
189 {
190 $defaultValues = $this->getCryptXOptionsDefaults();
191 $currentOptions = get_option('cryptX');
192
193 return wp_parse_args($currentOptions, $defaultValues);
194 }
195
196 /**
197 * Saves the cryptX options by updating the 'cryptX' option with the saved options merged with the default options.
198 *
199 * @param array $saveOptions The options to be saved.
200 *
201 * @return void
202 */
203 public function saveCryptXOptions(array $saveOptions): void
204 {
205 update_option('cryptX', wp_parse_args($saveOptions, $this->loadCryptXOptionsWithDefaults()));
206 }
207
208 /**
209 * Decodes attributes from their encoded state and returns the decoded array.
210 *
211 * @param array $attributes The array of attributes, potentially encoded.
212 * @return array The array of decoded attributes with the 'encoded' key removed if present.
213 */
214 private function decodeAttributes(array $attributes): array
215 {
216 if (($attributes['encoded'] ?? '') !== 'true') {
217 return $attributes;
218 }
219
220 $decodedAttributes = array_map(
221 fn($value) => $this->decodeString($value),
222 $attributes
223 );
224 unset($decodedAttributes['encoded']);
225
226 return $decodedAttributes;
227 }
228
229 /**
230 * Processes the provided shortcode attributes and content, encrypts content, and optionally creates links for email addresses.
231 *
232 * @param array $atts Attributes passed to the shortcode. Defaults to an empty array.
233 * @param string $content The content enclosed within the shortcode. Defaults to an empty string.
234 * @param string $tag The name of the shortcode tag. Defaults to an empty string.
235 * @return string The processed and encrypted content, optionally including links for email addresses.
236 */
237 public function cryptXShortcode(array $atts = [], string $content = '', string $tag = ''): string
238 {
239 // Decode attributes if needed
240 $attributes = $this->decodeAttributes($atts);
241
242 // Update options if attributes provided
243 if (!empty($attributes)) {
244 self::$cryptXOptions = shortcode_atts(
245 $this->loadCryptXOptionsWithDefaults(),
246 array_change_key_case($attributes, CASE_LOWER),
247 $tag
248 );
249 }
250
251 // Process content (inline the encryptAndLinkContent logic)
252 if (self::$cryptXOptions['autolink'] ?? false) {
253 $content = $this->addLinkToEmailAddresses($content, true);
254 }
255
256 $content = $this->findEmailAddressesInContent($content, true);
257 $processedContent = $this->replaceEmailInContent($content, true);
258
259 // Reset options to defaults
260 self::$cryptXOptions = $this->loadCryptXOptionsWithDefaults();
261
262 return $processedContent;
263 }
264
265 /**
266 * Encrypts and links content.
267 *
268 * @param string $content The content to be encrypted and linked.
269 *
270 * @return string The encrypted and linked content.
271 */
272 private function encryptAndLinkContent(string $content, bool $shortcode = false): string
273 {
274 $content = $this->findEmailAddressesInContent($content, $shortcode);
275
276 return $this->replaceEmailInContent($content, $shortcode);
277 }
278
279 /**
280 * Retrieves the ID of the current post.
281 *
282 * @return int The current post ID if available, or -1 if no post object is present.
283 */
284 private function getCurrentPostId(): int
285 {
286 global $post;
287 return (is_object($post)) ? $post->ID : -1;
288 }
289
290
291 /**
292 * Generates and returns a tiny URL image.
293 *
294 * @return void
295 */
296 public function cryptXtinyUrl(): void
297 {
298 $url = $_SERVER['REQUEST_URI'];
299 $params = explode('/', $url);
300 if (count($params) > 1) {
301 $tiny_url = $params[count($params) - 2];
302 if ($tiny_url == md5(get_bloginfo('url'))) {
303 $font = CRYPTX_DIR_PATH . 'fonts/' . self::$cryptXOptions['c2i_font'];
304 $msg = $params[count($params) - 1];
305 $size = self::$cryptXOptions['c2i_fontSize'];
306 $pad = 1;
307 $transparent = 1;
308 $rgb = str_replace("#", "", self::$cryptXOptions['c2i_fontRGB']);
309 $red = hexdec(substr($rgb, 0, 2));
310 $grn = hexdec(substr($rgb, 2, 2));
311 $blu = hexdec(substr($rgb, 4, 2));
312 $bg_red = 255 - $red;
313 $bg_grn = 255 - $grn;
314 $bg_blu = 255 - $blu;
315 $width = 0;
316 $height = 0;
317 $offset_x = 0;
318 $offset_y = 0;
319 $bounds = array();
320 $image = "";
321 $bounds = ImageTTFBBox($size, 0, $font, "W");
322 $font_height = abs($bounds[7] - $bounds[1]);
323 $bounds = ImageTTFBBox($size, 0, $font, $msg);
324 $width = abs($bounds[4] - $bounds[6]);
325 $height = abs($bounds[7] - $bounds[1]);
326 $offset_y = $font_height + abs(($height - $font_height) / 2) - 1;
327 $offset_x = 0;
328 $image = imagecreatetruecolor($width + ($pad * 2), $height + ($pad * 2));
329 imagesavealpha($image, true);
330 $foreground = ImageColorAllocate($image, $red, $grn, $blu);
331 $background = imagecolorallocatealpha($image, 0, 0, 0, 127);
332 imagefill($image, 0, 0, $background);
333 ImageTTFText($image, $size, 0, round($offset_x + $pad, 0), round($offset_y + $pad, 0), $foreground, $font, $msg);
334 Header("Content-type: image/png");
335 imagePNG($image);
336 die;
337 }
338 }
339 }
340
341 /**
342 * Adds common filters to a given filter name.
343 *
344 * This function adds the common filter 'autolink' to the provided $filterName.
345 *
346 * @param string $filterName The name of the filter to add common filters to.
347 *
348 * @return void
349 */
350 private function addAutoLinkFilters(string $filterName, $prio = 5): void
351 {
352 add_filter($filterName, [$this, 'addLinkToEmailAddresses'], $prio);
353 }
354
355 /**
356 * Adds additional filters to a given filter name.
357 *
358 * This function adds two additional filters, 'encryptx' and 'replaceEmailInContent',
359 * to the specified filter name. The 'encryptx' filter is added with a priority of 12,
360 * and the 'replaceEmailInContent' filter is added with a priority of 13.
361 *
362 * @param string $filterName The name of the filter to add the additional filters to.
363 *
364 * @return void
365 */
366 private function addOtherFilters(string $filterName): void
367 {
368 // Check if this is a widget filter
369 $widgetFilters = $this->config->getWidgetFilters();
370 $isWidgetFilter = in_array($filterName, $widgetFilters);
371
372 if ($isWidgetFilter) {
373 // Use higher priority for widget filters (after autolink at priority 10)
374 add_filter($filterName, [$this, 'findEmailAddressesInContent'], 15);
375 add_filter($filterName, [$this, 'replaceEmailInContent'], 16);
376 } else {
377 // Standard priorities for other filters
378 add_filter($filterName, [$this, 'findEmailAddressesInContent'], 12);
379 add_filter($filterName, [$this, 'replaceEmailInContent'], 13);
380 }
381 }
382
383
384 /**
385 * Adds and applies widget filters from the configuration.
386 *
387 * @return void
388 */
389 private function addWidgetFilters(): void
390 {
391 $widgetFilters = $this->config->getWidgetFilters();
392
393 foreach ($widgetFilters as $widgetFilter) {
394 $this->addAutoLinkFilters($widgetFilter, 11);
395 $this->addOtherFilters($widgetFilter);
396 }
397 }
398
399 /**
400 * Checks if a given ID is excluded based on the 'excludedIDs' variable.
401 *
402 * @param int $ID The ID to check if excluded.
403 *
404 * @return bool Returns true if the ID is excluded, false otherwise.
405 */
406 private function isIdExcluded(int $ID): bool
407 {
408 $excludedIds = explode(",", self::$cryptXOptions['excludedIDs']);
409
410 return in_array($ID, $excludedIds);
411 }
412
413 /**
414 * Replaces email addresses in content with link texts.
415 *
416 * @param string|null $content The content to replace the email addresses in.
417 * @param bool $isShortcode Flag indicating whether the method is called from a shortcode.
418 *
419 * @return string|null The content with replaced email addresses.
420 */
421 public function replaceEmailInContent(?string $content, bool $isShortcode = false): ?string
422 {
423 global $post;
424
425 if (self::$cryptXOptions['disable_rss'] && $this->isRssFeed()) return $content;
426
427 // Check if current filter is a widget filter
428 $widgetFilters = $this->config->getWidgetFilters();
429 $isWidgetContext = in_array(current_filter(), $widgetFilters);
430
431 $postId = (is_object($post)) ? $post->ID : -1;
432
433 // For widgets, always process; for other content, check exclusion rules
434 if (($isWidgetContext || !$this->isIdExcluded($postId) || $isShortcode) && !empty($content)) {
435 $content = $this->replaceEmailWithLinkText($content);
436 }
437
438 return $content;
439 }
440
441
442 /**
443 * Replace email addresses in a given content with link text.
444 *
445 * @param string $content The content to search for email addresses.
446 *
447 * @return string The content with email addresses replaced with link text.
448 */
449 private function replaceEmailWithLinkText(string $content): string
450 {
451 $emailPattern = "/([_a-zA-Z0-9-+]+(\.[_a-zA-Z0-9-+]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(\.[a-zA-Z]{2,}))/i";
452
453 return preg_replace_callback($emailPattern, [$this, 'encodeEmailToLinkText'], $content);
454 }
455
456 /**
457 * Encode email address to link text.
458 *
459 * @param array $Match The matched email address.
460 *
461 * @return string The encoded link text.
462 */
463 private function encodeEmailToLinkText(array $Match): string
464 {
465 if ($this->inWhiteList($Match)) {
466 return $Match[1];
467 }
468 switch (self::$cryptXOptions['opt_linktext']) {
469 case 1:
470 $text = $this->getLinkText();
471 break;
472 case 2:
473 $text = $this->getLinkImage();
474 break;
475 case 3:
476 $img_url = wp_get_attachment_url(self::$cryptXOptions['alt_uploadedimage']);
477 $text = $this->getUploadedImage($img_url);
478 self::$imageCounter++;
479 break;
480 case 4:
481 $text = antispambot($Match[1]);
482 break;
483 case 5:
484 $text = $this->getImageFromText($Match);
485 self::$imageCounter++;
486 break;
487 default:
488 $text = $this->getDefaultLinkText($Match);
489 }
490
491 return $text;
492 }
493
494 /**
495 * Check if the given match is in the whitelist.
496 *
497 * @param array $Match The match to check against the whitelist.
498 *
499 * @return bool True if the match is in the whitelist, false otherwise.
500 */
501 private function inWhiteList(array $Match): bool
502 {
503 $whiteList = array_filter(array_map('trim', explode(",", self::$cryptXOptions['whiteList'])));
504 $tmp = explode(".", $Match[0]);
505
506 return in_array(end($tmp), $whiteList);
507 }
508
509 /**
510 * Get the link text from cryptXOptions
511 *
512 * @return string The link text
513 */
514 private function getLinkText(): string
515 {
516 return self::$cryptXOptions['alt_linktext'];
517 }
518
519 /**
520 * Generate an HTML image tag with the link image URL as the source
521 *
522 * @return string The HTML image tag
523 */
524 private function getLinkImage(): string
525 {
526 return "<img src=\"" . self::$cryptXOptions['alt_linkimage'] . "\" class=\"cryptxImage\" alt=\"" . self::$cryptXOptions['alt_linkimage_title'] . "\" title=\"" . antispambot(self::$cryptXOptions['alt_linkimage_title']) . "\" />";
527 }
528
529 /**
530 * Get the HTML tag for an uploaded image.
531 *
532 * @param string $img_url The URL of the image.
533 *
534 * @return string The HTML tag for the image.
535 */
536 private function getUploadedImage(string $img_url): string
537 {
538 return "<img src=\"" . $img_url . "\" class=\"cryptxImage cryptxImage_" . self::$imageCounter . "\" alt=\"" . self::$cryptXOptions['http_linkimage_title'] . " title=\"" . antispambot(self::$cryptXOptions['http_linkimage_title']) . "\" />";
539 }
540
541 /**
542 * Converts a matched image URL into an HTML image element with cryptX classes and attributes.
543 *
544 * @param array $Match The matched image URL and other related data.
545 *
546 * @return string Returns the HTML image element.
547 */
548 private function getImageFromText(array $Match): string
549 {
550 return "<img src=\"" . get_bloginfo('url') . "/" . md5(get_bloginfo('url')) . "/" . antispambot($Match[1]) . "\" class=\"cryptxImage cryptxImage_" . self::$imageCounter . "\" alt=\"" . antispambot($Match[1]) . "\" title=\"" . antispambot($Match[1]) . "\" />";
551 }
552
553 /**
554 * Replaces specific characters with values from cryptX options in a given string.
555 *
556 * @param array $Match The array containing matches from a regular expression search.
557 * Array format: `[0 => string, 1 => string, ...]`.
558 * The first element is ignored, and the second element is used as input string.
559 *
560 * @return string The string with replaced characters or the original array if no matches were found.
561 * If the input string is an array, the function returns an array with replaced characters
562 * for each element.
563 */
564 private function getDefaultLinkText(array $Match): string
565 {
566 $text = str_replace("@", self::$cryptXOptions['at'], $Match[1]);
567
568 return str_replace(".", self::$cryptXOptions['dot'], $text);
569 }
570
571 /**
572 * List all files in a directory that match the given filter.
573 *
574 * @param string $path The path of the directory to list files from.
575 * @param array $filter The file extensions to filter by.
576 * If it's a string, it will be converted to an array of a single element.
577 *
578 * @return array An array of file names that match the filter.
579 */
580 public function getFilesInDirectory(string $path, array $filter): array
581 {
582 $directoryHandle = opendir($path);
583 $directoryContent = array();
584 while ($file = readdir($directoryHandle)) {
585 $fileExtension = substr(strtolower($file), -3);
586 if (in_array($fileExtension, $filter)) {
587 $directoryContent[] = $file;
588 }
589 }
590
591 return $directoryContent;
592 }
593
594 /**
595 * Finds and processes email addresses within the given content.
596 *
597 * This method scans the provided content for email addresses and encrypts them based on the configuration.
598 * It checks for RSS feed settings and excluded post IDs to determine whether encryption should be applied.
599 *
600 * @param string|null $content The content to search for email addresses. If null, the method returns null.
601 * @param bool $shortcode Specifies whether the method is invoked via a shortcode.
602 * @return string|null The processed content with email addresses encrypted, or null if the input content is null.
603 */
604 public function findEmailAddressesInContent(?string $content, bool $shortcode = false): ?string
605 {
606 global $post;
607
608 if (self::$cryptXOptions['disable_rss'] && $this->isRssFeed()) return $content;
609
610 if ($content === null) {
611 return null;
612 }
613
614 // Check if current filter is a widget filter
615 $widgetFilters = $this->config->getWidgetFilters();
616 $isWidgetContext = in_array(current_filter(), $widgetFilters);
617
618 $postId = (is_object($post)) ? $post->ID : -1;
619 $isIdExcluded = $this->isIdExcluded($postId);
620
621 $mailtoRegex = '/<a\s+[^>]*href=(["\'])mailto:([^"\']+)\1[^>]*>(.*?)<\/a>/is';
622
623 // For widgets, always process since there's no specific post context
624 // For other content, check exclusion rules
625 if ($isWidgetContext || !$isIdExcluded || $shortcode) {
626 // $content = preg_replace_callback($mailtoRegex, [$this, 'encryptEmailAddressNew'], $content);
627 $content = preg_replace_callback($mailtoRegex, [$this, 'encryptEmailAddressSecure'], $content);
628 }
629
630 return $content;
631 }
632
633
634 /**
635 * Encrypts email addresses in search results.
636 *
637 * @param array $searchResults The search results containing email addresses.
638 *
639 * @return string The search results with encrypted email addresses.
640 */
641 private function encryptEmailAddress(array $searchResults): string
642 {
643 $originalValue = $searchResults[0];
644
645 if (strpos($searchResults[self::INDEX_TO_CHECK], '@') === self::NOT_FOUND) {
646 return $originalValue;
647 }
648
649 $mailReference = self::MAIL_IDENTIFIER . $searchResults[self::INDEX_TO_CHECK];
650
651 if (str_starts_with($searchResults[self::INDEX_TO_CHECK], self::SUBJECT_IDENTIFIER)) {
652 return $originalValue;
653 }
654
655 $return = $originalValue;
656
657 // Apply JavaScript handler if enabled
658 if (!empty(self::$cryptXOptions['java'])) {
659 $javaHandler = "javascript:DeCryptX('" . $this->generateHashFromString($searchResults[self::INDEX_TO_CHECK]) . "')";
660 $return = str_replace(self::MAIL_IDENTIFIER . $searchResults[self::INDEX_TO_CHECK], $javaHandler, $originalValue);
661 } else {
662 // Only apply antispambot if JavaScript is not enabled
663 $return = str_replace($mailReference, antispambot($mailReference), $return);
664 }
665
666 // Add CSS attributes if specified
667 if (!empty(self::$cryptXOptions['css_id'])) {
668 $return = preg_replace(self::PATTERN, '$1" id="' . self::$cryptXOptions['css_id'] . '">', $return);
669 }
670
671 if (!empty(self::$cryptXOptions['css_class'])) {
672 $return = preg_replace(self::PATTERN, '$1" class="' . self::$cryptXOptions['css_class'] . '">', $return);
673 }
674
675 return $return;
676 }
677
678 /**
679 * Encrypts an email address within the provided search results and generates a secure or obfuscated link.
680 * If secure encryption is enabled, the function uses secure encryption. Otherwise, it falls back to legacy methods
681 * or antispambot obfuscation if JavaScript is not enabled. Additional CSS attributes can be added if specified.
682 *
683 * @param array $searchResults The array containing match results:
684 * - Index 0: The full match value (original string),
685 * - Index 2: The email address to encrypt,
686 * - Index 3: The link text for the email link.
687 * @return string Returns the modified string where the email address is encrypted or obfuscated based on the configuration.
688 */
689 private function encryptEmailAddressNew(array $searchResults): string
690 {
691 $originalValue = $searchResults[0]; // Full match
692 $emailAddress = $searchResults[2]; // Email address (now at index 2)
693 $linkText = $searchResults[3]; // Link text (now at index 3)
694
695 if (strpos($emailAddress, '@') === self::NOT_FOUND) {
696 return $originalValue;
697 }
698
699 if (str_starts_with($emailAddress, self::SUBJECT_IDENTIFIER)) {
700 return $originalValue;
701 }
702
703 $return = $originalValue;
704
705 // Apply JavaScript handler if enabled
706 if (!empty(self::$cryptXOptions['java'])) {
707 // Check if secure encryption is enabled and working
708 if ($this->config->isSecureEncryptionEnabled()) {
709 try {
710 // Use secure encryption - encrypt the full mailto URL
711 $password = $this->config->getEncryptionPassword();
712 $mailtoUrl = 'mailto:' . $emailAddress;
713 $encryptedEmail = SecureEncryption::encrypt($mailtoUrl, $password);
714
715 $javaHandler = "javascript:secureDecryptAndNavigate('" .
716 $this->escapeJavaScript($encryptedEmail) . "', '" .
717 $this->escapeJavaScript($password) . "')";
718 } catch (\Exception $e) {
719 // Fallback to legacy encryption if secure encryption fails
720 error_log('CryptX Secure Encryption failed: ' . $e->getMessage());
721 $encryptedEmail = $this->generateHashFromString($emailAddress);
722 $javaHandler = "javascript:DeCryptX('" . $this->escapeJavaScript($encryptedEmail) . "')";
723 }
724 } else {
725 // Use legacy encryption
726 $encryptedEmail = $this->generateHashFromString($emailAddress);
727 $javaHandler = "javascript:DeCryptX('" . $this->escapeJavaScript($encryptedEmail) . "')";
728 }
729
730 $return = str_replace('mailto:' . $emailAddress, $javaHandler, $originalValue);
731 } else {
732 // Fallback to antispambot if JavaScript is not enabled
733 $return = str_replace('mailto:' . $emailAddress,
734 antispambot('mailto:' . $emailAddress), $return);
735 }
736
737 // Add CSS attributes if specified
738 if (!empty(self::$cryptXOptions['css_id'])) {
739 $return = preg_replace('/(<a\s+[^>]*)(>)/i',
740 '$1 id="' . self::$cryptXOptions['css_id'] . '"$2', $return);
741 }
742
743 if (!empty(self::$cryptXOptions['css_class'])) {
744 $return = preg_replace('/(<a\s+[^>]*)(>)/i',
745 '$1 class="' . self::$cryptXOptions['css_class'] . '"$2', $return);
746 }
747
748 return $return;
749 }
750
751 /**
752 * Generate a hash string for the given input string.
753 *
754 * @param string $inputString The input string to generate a hash for.
755 *
756 * @return string The generated hash string.
757 */
758 private function generateHashFromString(string $inputString): string
759 {
760 $inputString = str_replace("&", "&", $inputString);
761 $crypt = '';
762
763 for ($i = 0; $i < strlen($inputString); $i++) {
764 do {
765 $salt = mt_rand(0, 3);
766 $asciiValue = ord(substr($inputString, $i)) + $salt;
767 if (8364 <= $asciiValue) {
768 $asciiValue = 128;
769 }
770 } while (in_array($asciiValue, self::ASCII_VALUES_BLACKLIST));
771
772 $crypt .= $salt . chr($asciiValue);
773 }
774
775 return $crypt;
776 }
777
778 /**
779 * add link to email addresses
780 */
781 /**
782 * Auto-link emails in the given content.
783 *
784 * @param string $content The content to process.
785 * @param bool $shortcode Whether the function is called from a shortcode or not.
786 *
787 * @return string The content with emails auto-linked.
788 */
789 public function addLinkToEmailAddresses(string $content, bool $shortcode = false): string
790 {
791 global $post;
792
793 // Check if current filter is a widget filter
794 $widgetFilters = $this->config->getWidgetFilters();
795 $isWidgetContext = in_array(current_filter(), $widgetFilters);
796
797 $postID = is_object($post) ? $post->ID : -1;
798
799 // For widgets, always process; for other content, check exclusion rules
800 if (!$isWidgetContext && $this->isIdExcluded($postID) && !$shortcode) {
801 return $content;
802 }
803
804 $emailPattern = "[_a-zA-Z0-9-+]+(\\.[_a-zA-Z0-9-+]+)*@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*(\\.[a-zA-Z]{2,})";
805 $linkPattern = "<a href=\"mailto:\\2\">\\2</a>";
806 $src = [
807 "/([\\s])($emailPattern)/si",
808 "/(>)($emailPattern)(<)/si",
809 "/(\\()($emailPattern)(\\))/si",
810 "/(>)($emailPattern)([\\s])/si",
811 "/([\\s])($emailPattern)(<)/si",
812 "/^($emailPattern)/si",
813 "/(<a[^>]*>)<a[^>]*>/",
814 "/(<\\/A>)<\\/A>/i"
815 ];
816 $tar = [
817 "\\1$linkPattern",
818 "\\1$linkPattern\\6",
819 "\\1$linkPattern\\6",
820 "\\1$linkPattern\\6",
821 "\\1$linkPattern\\6",
822 "<a href=\"mailto:\\0\">\\0</a>",
823 "\\1",
824 "\\1"
825 ];
826
827 return preg_replace($src, $tar, $content);
828 }
829
830 /**
831 * Installs the CryptX plugin by updating its options and loading default values.
832 */
833 public function installCryptX(): void
834 {
835 global $wpdb;
836 self::$cryptXOptions['admin_notices_deprecated'] = true;
837 if (self::$cryptXOptions['excludedIDs'] == "") {
838 $tmp = array();
839 $excludes = $wpdb->get_results("SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'cryptxoff' AND meta_value = 'true'");
840 if (count($excludes) > 0) {
841 foreach ($excludes as $exclude) {
842 $tmp[] = $exclude->post_id;
843 }
844 sort($tmp);
845 self::$cryptXOptions['excludedIDs'] = implode(",", $tmp);
846 update_option('cryptX', self::$cryptXOptions);
847 self::$cryptXOptions = $this->loadCryptXOptionsWithDefaults(); // reread Options
848 $wpdb->query("DELETE FROM $wpdb->postmeta WHERE meta_key = 'cryptxoff'");
849 }
850 }
851 if (empty(self::$cryptXOptions['c2i_font'])) {
852 self::$cryptXOptions['c2i_font'] = CRYPTX_DIR_PATH . 'fonts/' . $firstFont[0];
853 }
854 if (empty(self::$cryptXOptions['c2i_fontSize'])) {
855 self::$cryptXOptions['c2i_fontSize'] = 10;
856 }
857 if (empty(self::$cryptXOptions['c2i_fontRGB'])) {
858 self::$cryptXOptions['c2i_fontRGB'] = '000000';
859 }
860 update_option('cryptX', self::$cryptXOptions);
861 self::$cryptXOptions = $this->loadCryptXOptionsWithDefaults(); // reread Options
862 }
863
864 private function addHooksHelper($function_name, $hook_name): void
865 {
866 if (function_exists($function_name)) {
867 call_user_func($function_name, 'cryptx', 'CryptX', [$this, 'metaCheckbox'], $hook_name);
868 } else {
869 add_action("dbx_{$hook_name}_sidebar", [$this, 'metaOptionFieldset']);
870 }
871 }
872
873 public function metaBox(): void
874 {
875 $this->addHooksHelper('add_meta_box', 'post');
876 $this->addHooksHelper('add_meta_box', 'page');
877 }
878
879 /**
880 * Displays a checkbox to disable CryptX for the current post or page.
881 *
882 * This function outputs HTML code for a checkbox that allows the user to disable CryptX
883 * functionality for the current post or page. If the current post or page ID is excluded
884 **/
885 public function metaCheckbox(): void
886 {
887 global $post;
888 ?>
889 <label><input type="checkbox" name="disable_cryptx_pageid" <?php if ($this->isIdExcluded($post->ID)) {
890 echo 'checked="checked"';
891 } ?>/>
892 Disable CryptX for this post/page</label>
893 <?php
894 }
895
896 /**
897 * Renders the CryptX option fieldset for the current post/page if the user has permission to edit posts.
898 * This fieldset allows the user to enable or disable CryptX for the current post/page.
899 *
900 * @return void
901 */
902 public function metaOptionFieldset(): void
903 {
904 global $post;
905 if (current_user_can('edit_posts')) { ?>
906 <fieldset id="cryptxoption" class="dbx-box">
907 <h3 class="dbx-handle">CryptX</h3>
908 <div class="dbx-content">
909 <label><input type="checkbox"
910 name="disable_cryptx_pageid" <?php if ($this->isIdExcluded($post->ID)) {
911 echo 'checked="checked"';
912 } ?>/> Disable CryptX for this post/page</label>
913 </div>
914 </fieldset>
915 <?php
916 }
917 }
918
919 /**
920 * Adds a post ID to the excluded list in the cryptX options.
921 *
922 * @param int $postId The post ID to be added to the excluded list.
923 *
924 * @return void
925 */
926 public function addPostIdToExcludedList(int $postId): void
927 {
928 $postId = wp_is_post_revision($postId) ?: $postId;
929 $excludedIds = $this->updateExcludedIdsList(self::$cryptXOptions['excludedIDs'], $postId);
930 self::$cryptXOptions['excludedIDs'] = implode(",", array_filter($excludedIds));
931 update_option('cryptX', self::$cryptXOptions);
932 }
933
934 /**
935 * Updates the excluded IDs list based on a given ID and the current list.
936 *
937 * @param string $excludedIds The current excluded IDs list, separated by commas.
938 * @param int $postId The ID to be updated in the excluded IDs list.
939 *
940 * @return array The updated excluded IDs list as an array, with the ID removed if it existed and added if necessary.
941 */
942 private function updateExcludedIdsList(string $excludedIds, int $postId): array
943 {
944 $excludedIdsArray = explode(",", $excludedIds);
945 $excludedIdsArray = $this->removePostIdFromExcludedIds($excludedIdsArray, $postId);
946 $excludedIdsArray = $this->addPostIdToExcludedIdsIfNecessary($excludedIdsArray, $postId);
947
948 return $this->makeExcludedIdsUniqueAndSorted($excludedIdsArray);
949 }
950
951 /**
952 * Removes a specific post ID from the array of excluded IDs.
953 *
954 * @param array $excludedIds The array of excluded IDs.
955 * @param int $postId The ID of the post to be removed from the excluded IDs.
956 *
957 * @return array The updated array of excluded IDs without the specified post ID.
958 */
959 private function removePostIdFromExcludedIds(array $excludedIds, int $postId): array
960 {
961 foreach ($excludedIds as $key => $id) {
962 if ($id == $postId) {
963 unset($excludedIds[$key]);
964 break;
965 }
966 }
967
968 return $excludedIds;
969 }
970
971 /**
972 * Adds the post ID to the list of excluded IDs if necessary.
973 *
974 * @param array $excludedIds The array of excluded IDs.
975 * @param int $postId The post ID to be added to the excluded IDs.
976 *
977 * @return array The updated array of excluded IDs.
978 */
979 private function addPostIdToExcludedIdsIfNecessary(array $excludedIds, int $postId): array
980 {
981 if (isset($_POST['disable_cryptx_pageid'])) {
982 $excludedIds[] = $postId;
983 }
984
985 return $excludedIds;
986 }
987
988 /**
989 * Makes the excluded IDs unique and sorted.
990 *
991 * @param array $excludedIds The array of excluded IDs.
992 *
993 * @return array The array of excluded IDs with duplicate values removed and sorted in ascending order.
994 */
995 private function makeExcludedIdsUniqueAndSorted(array $excludedIds): array
996 {
997 $excludedIds = array_unique($excludedIds);
998 sort($excludedIds);
999
1000 return $excludedIds;
1001 }
1002
1003 /**
1004 * Displays a message in a styled div.
1005 *
1006 * @param string $message The message to be displayed.
1007 * @param bool $errormsg Optional. Indicates whether the message is an error message. Default is false.
1008 *
1009 * @return void
1010 */
1011 private function showMessage(string $message, bool $errormsg = false): void
1012 {
1013 if ($errormsg) {
1014 echo '<div id="message" class="error">';
1015 } else {
1016 echo '<div id="message" class="updated fade">';
1017 }
1018
1019 echo "$message</div>";
1020 }
1021
1022 /**
1023 * Retrieves the domain from the current site URL.
1024 *
1025 * @return string The domain of the current site URL.
1026 */
1027 public function getDomain(): string
1028 {
1029 return $this->trimSlashFromDomain($this->removeProtocolFromUrl($this->getSiteUrl()));
1030 }
1031
1032 /**
1033 * Retrieves the site URL.
1034 *
1035 * @return string The site URL.
1036 */
1037 private function getSiteUrl(): string
1038 {
1039 return get_option('siteurl');
1040 }
1041
1042 /**
1043 * Removes the protocol from a URL.
1044 *
1045 * @param string $url The URL string to remove the protocol from.
1046 *
1047 * @return string The URL string without the protocol.
1048 */
1049 private function removeProtocolFromUrl(string $url): string
1050 {
1051 return preg_replace('|https?://|', '', $url);
1052 }
1053
1054 /**
1055 * Trims the trailing slash from a domain.
1056 *
1057 * @param string $domain The domain to trim the slash from.
1058 *
1059 * @return string The domain with the trailing slash removed.
1060 */
1061 private function trimSlashFromDomain(string $domain): string
1062 {
1063 if ($slashPosition = strpos($domain, '/')) {
1064 $domain = substr($domain, 0, $slashPosition);
1065 }
1066
1067 return $domain;
1068 }
1069
1070 /**
1071 * Loads Javascript files required for CryptX functionality.
1072 *
1073 * @return void
1074 */
1075 public function loadJavascriptFiles(): void
1076 {
1077 wp_enqueue_script('cryptx-js', CRYPTX_DIR_URL . 'js/cryptx.min.js', false, false, self::$cryptXOptions['load_java']);
1078 wp_enqueue_style('cryptx-styles', CRYPTX_DIR_URL . 'css/cryptx.css');
1079 }
1080
1081 /**
1082 * Updates the CryptX settings.
1083 *
1084 * This method retrieves the current CryptX options from the database and checks if the version of CryptX
1085 * stored in the options is less than the current version of CryptX. If the version is outdated, the method
1086 * updates the necessary settings and saves the updated options back to the database.
1087 *
1088 * @return void
1089 */
1090 private function updateCryptXSettings(): void
1091 {
1092 self::$cryptXOptions = get_option('cryptX');
1093 if (isset(self::$cryptXOptions['version']) && version_compare(CRYPTX_VERSION, self::$cryptXOptions['version']) > 0) {
1094 if (isset(self::$cryptXOptions['version'])) {
1095 unset(self::$cryptXOptions['version']);
1096 }
1097 if (isset(self::$cryptXOptions['c2i_font'])) {
1098 unset(self::$cryptXOptions['c2i_font']);
1099 }
1100 if (isset(self::$cryptXOptions['c2i_fontRGB'])) {
1101 self::$cryptXOptions['c2i_fontRGB'] = "#" . self::$cryptXOptions['c2i_fontRGB'];
1102 }
1103 if (isset(self::$cryptXOptions['alt_uploadedimage']) && !is_int(self::$cryptXOptions['alt_uploadedimage'])) {
1104 unset(self::$cryptXOptions['alt_uploadedimage']);
1105 if (self::$cryptXOptions['opt_linktext'] == 3) {
1106 unset(self::$cryptXOptions['opt_linktext']);
1107 }
1108 }
1109 self::$cryptXOptions = wp_parse_args(self::$cryptXOptions, $this->getCryptXOptionsDefaults());
1110 update_option('cryptX', self::$cryptXOptions);
1111 }
1112 }
1113
1114 /**
1115 * Encodes a string by replacing special characters with their corresponding HTML entities.
1116 *
1117 * @param string|null $str The string to be encoded.
1118 *
1119 * @return string The encoded string, or an array of encoded strings if an array was passed.
1120 */
1121 private function encodeString(?string $str): string
1122 {
1123 $str = htmlentities($str, ENT_QUOTES, 'UTF-8');
1124 $special = array(
1125 '[' => '&#91;',
1126 ']' => '&#93;',
1127 );
1128
1129 return str_replace(array_keys($special), array_values($special), $str);
1130 }
1131
1132 /**
1133 * Decodes a string that has been HTML entity encoded.
1134 *
1135 * @param string|null $str The string to decode. If null, an empty string is returned.
1136 *
1137 * @return string The decoded string.
1138 */
1139 private function decodeString(?string $str): string
1140 {
1141 return html_entity_decode($str, ENT_QUOTES, 'UTF-8');
1142 }
1143
1144 /**
1145 * Converts an associative array into an argument string.
1146 *
1147 * @param array $args An optional associative array where keys represent argument names and values represent argument values.
1148 * @return string A formatted string of arguments where each key-value pair is encoded and concatenated.
1149 */
1150 public function convertArrayToArgumentString(array $args = []): string
1151 {
1152 $string = "";
1153 if (!empty($args)) {
1154 foreach ($args as $key => $value) {
1155 $string .= sprintf(" %s=\"%s\"", $key, $this->encodeString($value));
1156 }
1157 $string .= " encoded=\"true\"";
1158 }
1159
1160 return $string;
1161 }
1162
1163 /**
1164 * Check if current request is for an RSS feed
1165 *
1166 * @return bool True if current request is for an RSS feed, false otherwise
1167 */
1168 private function isRssFeed(): bool
1169 {
1170 return is_feed();
1171 }
1172
1173 /**
1174 * Adds plugin action links to the WordPress plugin row
1175 *
1176 * @param array $links Existing plugin row links
1177 * @param string $file Plugin file path
1178 * @return array Modified plugin row links
1179 */
1180 public function add_plugin_action_links(array $links, string $file): array
1181 {
1182 if ($file !== CRYPTX_BASENAME) {
1183 return $links;
1184 }
1185
1186 $additional_links = [
1187 $this->create_settings_link(),
1188 $this->create_donation_link()
1189 ];
1190
1191 return array_merge($links, $additional_links);
1192 }
1193
1194 /**
1195 * Creates and returns a settings link for the options page.
1196 *
1197 * @return string The HTML link to the settings page.
1198 */
1199 private function create_settings_link(): string
1200 {
1201 return sprintf(
1202 '<a href="options-general.php?page=%s">%s</a>',
1203 CRYPTX_BASEFOLDER,
1204 __('Settings')
1205 );
1206 }
1207
1208 /**
1209 * Creates and returns a donation link in HTML format.
1210 *
1211 * @return string The HTML string for the donation link.
1212 */
1213 private function create_donation_link(): string
1214 {
1215 return sprintf(
1216 '<a href="%s">%s</a>',
1217 self::PAYPAL_DONATION_URL,
1218 __('Donate', 'cryptx')
1219 );
1220 }
1221
1222 /**
1223 * Adds a universal filter for all widget types by hooking into the widget display process.
1224 *
1225 * @return void
1226 */
1227 private function addUniversalWidgetFilters(): void
1228 {
1229 // Hook into the widget display process to catch all widget types
1230 add_filter('widget_display_callback', [$this, 'processWidgetContent'], 10, 3);
1231 }
1232
1233 /**
1234 * Processes the widget content to detect and modify email addresses.
1235 *
1236 * @param array $instance The current widget instance settings.
1237 * @param object $widget The widget object being processed.
1238 * @param array $args Additional arguments passed by the widget function.
1239 *
1240 * @return array The modified widget instance with updated content.
1241 */
1242 public function processWidgetContent($instance, $widget, $args)
1243 {
1244 // Only process if widget_text option is enabled
1245 if (!(self::$cryptXOptions['widget_text'] ?? false)) {
1246 return $instance;
1247 }
1248
1249 // Check if instance has text content (traditional text widgets)
1250 if (isset($instance['text']) && stripos($instance['text'], '@') !== false) {
1251 $instance['text'] = $this->addLinkToEmailAddresses($instance['text']);
1252 $instance['text'] = $this->findEmailAddressesInContent($instance['text']);
1253 $instance['text'] = $this->replaceEmailInContent($instance['text']);
1254 }
1255
1256 // Check if instance has content field (block widgets)
1257 if (isset($instance['content']) && stripos($instance['content'], '@') !== false) {
1258 $instance['content'] = $this->addLinkToEmailAddresses($instance['content']);
1259 $instance['content'] = $this->findEmailAddressesInContent($instance['content']);
1260 $instance['content'] = $this->replaceEmailInContent($instance['content']);
1261 }
1262
1263 return $instance;
1264 }
1265
1266 /**
1267 * Generates hash using secure or legacy encryption based on settings
1268 *
1269 * @param string $inputString
1270 * @return string
1271 */
1272 private function generateSecureHashFromString(string $inputString): string
1273 {
1274 if ($this->config->isSecureEncryptionEnabled()) {
1275 try {
1276 $password = $this->config->getEncryptionPassword();
1277 return SecureEncryption::encrypt($inputString, $password);
1278 } catch (\Exception $e) {
1279 error_log('CryptX Secure Encryption failed: ' . $e->getMessage());
1280 // Fallback to legacy encryption
1281 return $this->generateHashFromString($inputString);
1282 }
1283 }
1284
1285 return $this->generateHashFromString($inputString);
1286 }
1287
1288 /**
1289 * Enhanced email encryption with security validation
1290 *
1291 * @param array $searchResults
1292 * @return string
1293 */
1294 private function encryptEmailAddressSecure(array $searchResults): string
1295 {
1296 $originalValue = $searchResults[0]; // Full match
1297 $emailAddress = $searchResults[2]; // Email address
1298 $linkText = $searchResults[3]; // Link text
1299
1300 if (strpos($emailAddress, '@') === self::NOT_FOUND) {
1301 return $originalValue;
1302 }
1303
1304 if (str_starts_with($emailAddress, self::SUBJECT_IDENTIFIER)) {
1305 return $originalValue;
1306 }
1307
1308 $return = $originalValue;
1309
1310 // Apply JavaScript handler if enabled
1311 if (!empty(self::$cryptXOptions['java'])) {
1312 $encryptionMode = $this->config->getEncryptionMode();
1313
1314 // Determine which encryption method to use
1315 if ($encryptionMode === 'secure' &&
1316 $this->config->isSecureEncryptionEnabled() &&
1317 class_exists('CryptX\SecureEncryption')) {
1318
1319 // Use modern AES-256-GCM encryption
1320 try {
1321 $password = $this->config->getEncryptionPassword();
1322 $mailtoUrl = 'mailto:' . $emailAddress;
1323 $encryptedEmail = SecureEncryption::encrypt($mailtoUrl, $password);
1324
1325 $javaHandler = "javascript:secureDecryptAndNavigate('" .
1326 $this->escapeJavaScript($encryptedEmail) . "', '" .
1327 $this->escapeJavaScript($password) . "')";
1328 } catch (\Exception $e) {
1329 // Fallback to legacy if secure encryption fails
1330 error_log('CryptX Secure Encryption failed, falling back to legacy: ' . $e->getMessage());
1331 $encryptedEmail = $this->generateHashFromString($emailAddress);
1332 $javaHandler = "javascript:DeCryptX('" . $this->escapeJavaScript($encryptedEmail) . "')";
1333 }
1334 } else {
1335 // Use legacy encryption (original algorithm)
1336 $encryptedEmail = $this->generateHashFromString($emailAddress);
1337 $javaHandler = "javascript:DeCryptX('" . $this->escapeJavaScript($encryptedEmail) . "')";
1338 }
1339
1340 $return = str_replace('mailto:' . $emailAddress, $javaHandler, $originalValue);
1341 } else {
1342 // Fallback to antispambot if JavaScript is not enabled
1343 $return = str_replace('mailto:' . $emailAddress,
1344 antispambot('mailto:' . $emailAddress), $return);
1345 }
1346
1347 // Add CSS attributes if specified
1348 if (!empty(self::$cryptXOptions['css_id'])) {
1349 $return = preg_replace('/(<a\s+[^>]*)(>)/i',
1350 '$1 id="' . self::$cryptXOptions['css_id'] . '"$2', $return);
1351 }
1352
1353 if (!empty(self::$cryptXOptions['css_class'])) {
1354 $return = preg_replace('/(<a\s+[^>]*)(>)/i',
1355 '$1 class="' . self::$cryptXOptions['css_class'] . '"$2', $return);
1356 }
1357
1358 return $return;
1359 }
1360
1361 /**
1362 * Escapes string for safe JavaScript usage
1363 *
1364 * @param string $string
1365 * @return string
1366 */
1367 private function escapeJavaScript(string $string): string
1368 {
1369 return str_replace(
1370 ['\\', "'", '"', "\n", "\r", "\t"],
1371 ['\\\\', "\\'", '\\"', '\\n', '\\r', '\\t'],
1372 $string
1373 );
1374 }
1375
1376 /**
1377 * Secure URL validation
1378 *
1379 * @param string $url
1380 * @return bool
1381 */
1382 private function isValidUrl(string $url): bool
1383 {
1384 return SecureEncryption::validateUrl($url);
1385 }
1386
1387 }