Plugin Directory

Changeset 3323475


Ignore:
Timestamp:
07/07/2025 11:17:17 AM (9 months ago)
Author:
d3395
Message:

released version 3.5.0

Location:
cryptx
Files:
30 added
10 edited
1 copied

Legend:

Unmodified
Added
Removed
  • cryptx/tags/3.5.0/classes/CryptX.php

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

    r3103653 r3323475  
    44 * Plugin URI: http://weber-nrw.de/wordpress/cryptx/
    55 * Description: No more SPAM by spiders scanning you site for email addresses. With CryptX you can hide all your email addresses, with and without a mailto-link, by converting them using javascript or UNICODE.
    6  * Version: 3.4.5.3
    7  * Requires at least: 6.0
     6 * Version: 3.5.0
     7 * Requires at least: 6.7
    88 * Author: Ralf Weber
    99 * Author URI: http://weber-nrw.de/
     
    2828define( 'CRYPTX_FILENAME', str_replace( CRYPTX_BASEFOLDER . '/', '', plugin_basename( __FILE__ ) ) );
    2929
    30 require_once( CRYPTX_DIR_PATH . 'classes/CryptX.php' );
    31 require_once( CRYPTX_DIR_PATH . 'include/admin_option_page.php' );
     30spl_autoload_register(function ($class_name) {
     31    // Handle classes in the CryptX namespace
     32    if (strpos($class_name, 'CryptX\\') === 0) {
     33        // Convert namespace separators to directory separators
     34        $file_path = str_replace('\\', DIRECTORY_SEPARATOR, $class_name);
     35        $file_path = str_replace('CryptX' . DIRECTORY_SEPARATOR, '', $file_path);
    3236
    33 $CryptX_instance = Cryptx\CryptX::getInstance();
     37        // Construct the full file path
     38        $file = CRYPTX_DIR_PATH . 'classes' . DIRECTORY_SEPARATOR . $file_path . '.php';
     39
     40        if (file_exists($file)) {
     41            require_once $file;
     42        }
     43    }
     44});
     45
     46
     47//require_once( CRYPTX_DIR_PATH . 'classes/CryptX.php' );
     48//require_once( CRYPTX_DIR_PATH . 'include/admin_option_page.php' );
     49
     50$CryptX_instance = CryptX\CryptX::get_instance();
    3451$CryptX_instance->startCryptX();
    3552
     
    4360 */
    4461function encryptx( ?string $content, ?array $args = [] ): string {
    45     $CryptX_instance = Cryptx\CryptX::getInstance();
     62    $CryptX_instance = Cryptx\CryptX::get_instance();
    4663    return do_shortcode( '[cryptx'. $CryptX_instance->convertArrayToArgumentString( $args ).']' . $content . '[/cryptx]' );
    4764}
  • cryptx/tags/3.5.0/js/cryptx.js

    r3102439 r3323475  
    3737    location.href=DeCryptString( encryptedUrl );
    3838}
     39
     40/**
     41 * Generates a hashed string from the input string using a custom algorithm.
     42 * The method applies a randomized salt to the ASCII values of the characters
     43 * in the input string while avoiding certain blacklisted ASCII values.
     44 *
     45 * @param {string} inputString - The input string to be hashed.
     46 * @return {string} The generated hash string.
     47 */
     48function generateHashFromString(inputString) {
     49    // Replace & with itself (this line seems redundant in the original PHP code)
     50    inputString = inputString.replace("&", "&");
     51    let crypt = '';
     52
     53    // ASCII values blacklist (taken from the PHP constant)
     54    const ASCII_VALUES_BLACKLIST = ['32', '34', '39', '60', '62', '63', '92', '94', '96', '127'];
     55
     56    for (let i = 0; i < inputString.length; i++) {
     57        let salt, asciiValue;
     58        do {
     59            // Generate random number between 0 and 3
     60            salt = Math.floor(Math.random() * 4);
     61            // Get ASCII value and add salt
     62            asciiValue = inputString.charCodeAt(i) + salt;
     63
     64            // Check if value exceeds limit
     65            if (8364 <= asciiValue) {
     66                asciiValue = 128;
     67            }
     68        } while (ASCII_VALUES_BLACKLIST.includes(asciiValue.toString()));
     69
     70        // Append salt and character to result
     71        crypt += salt + String.fromCharCode(asciiValue);
     72    }
     73
     74    return crypt;
     75}
     76
     77/**
     78 * Generates a DeCryptX handler URL with a hashed email address.
     79 *
     80 * @param {string} emailAddress - The email address to be hashed and included in the handler URL.
     81 * @return {string} A string representing the JavaScript DeCryptX handler with the hashed email address.
     82 */
     83function generateDeCryptXHandler(emailAddress) {
     84    return `javascript:DeCryptX('${generateHashFromString(emailAddress)}')`;
     85}
  • cryptx/tags/3.5.0/js/cryptx.min.js

    r1111020 r3323475  
    1 function DeCryptString(r){for(var t=0,n="mailto:",o=0,e=0;e<r.length/2;e++)o=r.substr(2*e,1),t=r.charCodeAt(2*e+1),t>=8364&&(t=128),n+=String.fromCharCode(t-o);return n}function DeCryptX(r){location.href=DeCryptString(r)}
     1const UPPER_LIMIT=8364,DEFAULT_VALUE=128;function DeCryptString(t){let r=0,e="mailto:",n=0;for(let o=0;o<t.length;o+=2)n=t.substr(o,1),r=t.charCodeAt(o+1),r>=8364&&(r=128),e+=String.fromCharCode(r-n);return e}function DeCryptX(t){location.href=DeCryptString(t)}function generateHashFromString(t){t=t.replace("&","&");let r="";const e=["32","34","39","60","62","63","92","94","96","127"];for(let n=0;n<t.length;n++){let o,a;do{o=Math.floor(4*Math.random()),a=t.charCodeAt(n)+o,8364<=a&&(a=128)}while(e.includes(a.toString()));r+=o+String.fromCharCode(a)}return r}function generateDeCryptXHandler(t){return`javascript:DeCryptX('${generateHashFromString(t)}')`}
  • cryptx/tags/3.5.0/readme.txt

    r3321843 r3323475  
    55Requires at least: 6.0
    66Tested up to: 6.8
    7 Stable tag: 3.4.5.3
    8 Requires PHP: 7.4
     7Stable tag: 3.5.0
     8Requires PHP: 8.0
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    2424
    2525== Changelog ==
     26= 3.5.0 =
     27* Parts of the code have been rewritten to make the plugin more maintainable.
     28* fixed some bugs
     29* added option to disable CryptX on RSS feeds (requested: https://wordpress.org/support/topic/cryptx-should-be-disabled-for-rss-content/)
     30* Added new Javascript function to add CryptX mailto links via javascript on client side (requested: https://wordpress.org/support/topic/javascript-function-to-encrypt-emails/)
    2631= 3.4.5.3 =
    2732* fixed a Critical error in combination with WPML
  • cryptx/trunk/classes/CryptX.php

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

    r3103653 r3323475  
    44 * Plugin URI: http://weber-nrw.de/wordpress/cryptx/
    55 * Description: No more SPAM by spiders scanning you site for email addresses. With CryptX you can hide all your email addresses, with and without a mailto-link, by converting them using javascript or UNICODE.
    6  * Version: 3.4.5.3
    7  * Requires at least: 6.0
     6 * Version: 3.5.0
     7 * Requires at least: 6.7
    88 * Author: Ralf Weber
    99 * Author URI: http://weber-nrw.de/
     
    2828define( 'CRYPTX_FILENAME', str_replace( CRYPTX_BASEFOLDER . '/', '', plugin_basename( __FILE__ ) ) );
    2929
    30 require_once( CRYPTX_DIR_PATH . 'classes/CryptX.php' );
    31 require_once( CRYPTX_DIR_PATH . 'include/admin_option_page.php' );
     30spl_autoload_register(function ($class_name) {
     31    // Handle classes in the CryptX namespace
     32    if (strpos($class_name, 'CryptX\\') === 0) {
     33        // Convert namespace separators to directory separators
     34        $file_path = str_replace('\\', DIRECTORY_SEPARATOR, $class_name);
     35        $file_path = str_replace('CryptX' . DIRECTORY_SEPARATOR, '', $file_path);
    3236
    33 $CryptX_instance = Cryptx\CryptX::getInstance();
     37        // Construct the full file path
     38        $file = CRYPTX_DIR_PATH . 'classes' . DIRECTORY_SEPARATOR . $file_path . '.php';
     39
     40        if (file_exists($file)) {
     41            require_once $file;
     42        }
     43    }
     44});
     45
     46
     47//require_once( CRYPTX_DIR_PATH . 'classes/CryptX.php' );
     48//require_once( CRYPTX_DIR_PATH . 'include/admin_option_page.php' );
     49
     50$CryptX_instance = CryptX\CryptX::get_instance();
    3451$CryptX_instance->startCryptX();
    3552
     
    4360 */
    4461function encryptx( ?string $content, ?array $args = [] ): string {
    45     $CryptX_instance = Cryptx\CryptX::getInstance();
     62    $CryptX_instance = Cryptx\CryptX::get_instance();
    4663    return do_shortcode( '[cryptx'. $CryptX_instance->convertArrayToArgumentString( $args ).']' . $content . '[/cryptx]' );
    4764}
  • cryptx/trunk/js/cryptx.js

    r3102439 r3323475  
    3737    location.href=DeCryptString( encryptedUrl );
    3838}
     39
     40/**
     41 * Generates a hashed string from the input string using a custom algorithm.
     42 * The method applies a randomized salt to the ASCII values of the characters
     43 * in the input string while avoiding certain blacklisted ASCII values.
     44 *
     45 * @param {string} inputString - The input string to be hashed.
     46 * @return {string} The generated hash string.
     47 */
     48function generateHashFromString(inputString) {
     49    // Replace & with itself (this line seems redundant in the original PHP code)
     50    inputString = inputString.replace("&", "&");
     51    let crypt = '';
     52
     53    // ASCII values blacklist (taken from the PHP constant)
     54    const ASCII_VALUES_BLACKLIST = ['32', '34', '39', '60', '62', '63', '92', '94', '96', '127'];
     55
     56    for (let i = 0; i < inputString.length; i++) {
     57        let salt, asciiValue;
     58        do {
     59            // Generate random number between 0 and 3
     60            salt = Math.floor(Math.random() * 4);
     61            // Get ASCII value and add salt
     62            asciiValue = inputString.charCodeAt(i) + salt;
     63
     64            // Check if value exceeds limit
     65            if (8364 <= asciiValue) {
     66                asciiValue = 128;
     67            }
     68        } while (ASCII_VALUES_BLACKLIST.includes(asciiValue.toString()));
     69
     70        // Append salt and character to result
     71        crypt += salt + String.fromCharCode(asciiValue);
     72    }
     73
     74    return crypt;
     75}
     76
     77/**
     78 * Generates a DeCryptX handler URL with a hashed email address.
     79 *
     80 * @param {string} emailAddress - The email address to be hashed and included in the handler URL.
     81 * @return {string} A string representing the JavaScript DeCryptX handler with the hashed email address.
     82 */
     83function generateDeCryptXHandler(emailAddress) {
     84    return `javascript:DeCryptX('${generateHashFromString(emailAddress)}')`;
     85}
  • cryptx/trunk/js/cryptx.min.js

    r1111020 r3323475  
    1 function DeCryptString(r){for(var t=0,n="mailto:",o=0,e=0;e<r.length/2;e++)o=r.substr(2*e,1),t=r.charCodeAt(2*e+1),t>=8364&&(t=128),n+=String.fromCharCode(t-o);return n}function DeCryptX(r){location.href=DeCryptString(r)}
     1const UPPER_LIMIT=8364,DEFAULT_VALUE=128;function DeCryptString(t){let r=0,e="mailto:",n=0;for(let o=0;o<t.length;o+=2)n=t.substr(o,1),r=t.charCodeAt(o+1),r>=8364&&(r=128),e+=String.fromCharCode(r-n);return e}function DeCryptX(t){location.href=DeCryptString(t)}function generateHashFromString(t){t=t.replace("&","&");let r="";const e=["32","34","39","60","62","63","92","94","96","127"];for(let n=0;n<t.length;n++){let o,a;do{o=Math.floor(4*Math.random()),a=t.charCodeAt(n)+o,8364<=a&&(a=128)}while(e.includes(a.toString()));r+=o+String.fromCharCode(a)}return r}function generateDeCryptXHandler(t){return`javascript:DeCryptX('${generateHashFromString(t)}')`}
  • cryptx/trunk/readme.txt

    r3321843 r3323475  
    55Requires at least: 6.0
    66Tested up to: 6.8
    7 Stable tag: 3.4.5.3
    8 Requires PHP: 7.4
     7Stable tag: 3.5.0
     8Requires PHP: 8.0
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    2424
    2525== Changelog ==
     26= 3.5.0 =
     27* Parts of the code have been rewritten to make the plugin more maintainable.
     28* fixed some bugs
     29* added option to disable CryptX on RSS feeds (requested: https://wordpress.org/support/topic/cryptx-should-be-disabled-for-rss-content/)
     30* Added new Javascript function to add CryptX mailto links via javascript on client side (requested: https://wordpress.org/support/topic/javascript-function-to-encrypt-emails/)
    2631= 3.4.5.3 =
    2732* fixed a Critical error in combination with WPML
Note: See TracChangeset for help on using the changeset viewer.