PluginProbe
CryptX / 3.5.2
CryptX v3.5.2
4.2.1 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 All 93 releases
cryptx / classes / CryptX.php

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

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