Plugin Directory

Changeset 3646096


Ignore:
Timestamp:
08/13/2026 07:43:04 PM (2 months ago)
Author:
kgmservizi
Message:

Update 2.6 version and remove old 2.4.1.

Location:
current-year-shortcode
Files:
10 added
8 edited

Legend:

Unmodified
Added
Removed
  • current-year-shortcode/trunk/index.php

    r1971581 r3646096  
    11<?php
    2 // Silence is golden.
     2/**
     3 * Silence is golden.
     4 *
     5 * @package Current_Year_Shortcode
     6 */
  • current-year-shortcode/trunk/readme.txt

    r3504475 r3646096  
    44Tags: current year shortcode, copyright shortcode, copyright symbol, trademark shortcode, symbol shortcode
    55Requires at least: 2.5
    6 Tested up to: 6.8
     6Tested up to: 7.0
    77Requires PHP: 5.6
    8 Stable tag: 2.5
     8Stable tag: 2.6
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    1818
    1919**How shortcode work:** [WordPress Codex](https://codex.wordpress.org/Shortcode "WordPress Codex")
    20 
    21 **Full documentation and shortcode list:** [Documentation](http://uskgm.it/cysdoc "Documentation")
    2220
    2321Current year, copyright, symbols and user IP with shortcode.
    … …  
    5149For shortcode with month you can choose format with attribute format="". It support "F" - "m" - "M" - "n".
    5250
     51"F" full name (April)
     52"M" short name (Apr)
     53"m" number with leading zero (04)
     54"n" number without leading zero (4)
     55
    5356**Format Attribute for Day**
    54 For shortcode with day you can choose format with attribute format="". It support "D" - "d" - "j" - "N"
     57For shortcode with day you can choose format with attribute format="". It support "d" - "D" - "j" - "N" - "S" - "w" - "z" - "t".
     58
     59"d" number with leading zero (05)
     60"j" number without leading zero (5)
     61"D" short day name (Mon)
     62"S" ordinal suffix, to put after "j" (st, nd, rd, th)
     63"N" day of the week, 1 for Monday to 7 for Sunday
     64"w" day of the week, 0 for Sunday to 6 for Saturday
     65"z" day of the year (0 to 365)
     66"t" number of days in the current month
    5567
    5668**Format Attribute for current date**
    57 All php date function attributes.
     69For [dmy] you can use the letters of the php date function, plus space and - / . , : ; characters.
     70Escaped literal characters (the ones with a backslash) are not supported.
     71
     72**Example**
     73[dmy format="d/m/Y"] **12/04/2026**
     74[dmy format="j F Y"] **12 April 2026**
     75[dmy format="Y-m-d"] **2026-04-12**
     76[dmy format="l"] **Sunday**
     77[dmy format="H:i"] **18:30**
     78
     79Month and day names come out in the language of your site.
    5880
    5981**Offset Attribute**
    60 For day shortcode [d] you can add offset with attribute offset="". It support + or -.
    61 For month shortcode [m] you can add offset with attribute offset="". It support + or -.
    62 For year shortcode [y] you can add offset with attribute offset="". It support + or -.
    63 For current date shortcode [dmy] you can add offset with attribute offset="". It support + or - in standard php date format, for ex. +1 year or -1 day.
     82For [y], [m] and [d] the offset is a plain number with + or -, from -1000 to 1000.
     83For [dmy] the number needs a unit after it: years, months, weeks, days, hours, minutes, seconds. Also "today", "yesterday" and "tomorrow" work.
    6484
    6585**Example**
    … …  
    6787[m offset="+1"] **May** (if current month is April)
    6888[y offset="+1"] **2027** (if current year is 2026)
     89[y offset="-3"] **2023** (if current year is 2026)
    6990[dmy offset="+1 year"] **12/04/2027**
     91[dmy offset="-2 months"] **12/02/2026**
     92[dmy offset="tomorrow"] **13/04/2026**
     93
     94Offsets follow the timezone of your site, the one in Settings > General.
    7095
    7196
    … …  
    101126[show_user_ip] display ip of current user
    102127
    103 **Full list here:** [Documentation](http://uskgm.it/cysdoc "Documentation")
     128The complete list of format letters and offsets is in the Description tab.
    104129
    105130= What are the symbols shortcode? =
    … …  
    119144With IP shortcode you can retrieve your user IP.
    120145
     146= I use a CDN and [show_user_ip] shows the wrong address =
     147
     148The shortcode reads a forwarded header only when the request reaches your site through a proxy it recognises. Local reverse proxies and Cloudflare are already in the list. For any other CDN, add its ranges:
     149
     150`add_filter( 'cys_trusted_proxies', function ( $proxies ) {
     151        $proxies[] = '203.0.113.0/24';
     152        return $proxies;
     153} );`
     154
     155Ask your host or your CDN which ranges to use. Getting it wrong breaks nothing, the shortcode simply falls back to the address of whoever opened the connection.
     156
     157= Why is the page with [show_user_ip] not cached? =
     158
     159Because that shortcode prints something different for every visitor, so the page has to be built on each request. If you also cache at the edge, exclude the page there as well.
     160
    121161= How work year format attribute? =
    122162
    … …  
    134174= How work day format attribute? =
    135175
    136 For shortcode with day you can choose format with attribute format="". It support "D" - "d" - "j" - "N"
     176For shortcode with day you can choose format with attribute format="". It support "d" - "D" - "j" - "N" - "S" - "w" - "z" - "t". The full list with the meaning of each letter is in the Description tab.
    137177
    138178= How work year, month and day offset attribute? =
    139179
    140 For day shortcode [d] you can add offset with attribute offset="". It support + or -.
    141 For month shortcode [m] you can add offset with attribute offset="". It support + or -.
    142 For year shortcode [y] you can add offset with attribute offset="". It support + or -.
    143 For current date shortcode [dmy] you can add offset with attribute offset="". It support + or - in standard php date format, for ex. +1 year or -1 day.
     180For [y], [m] and [d] the offset is a plain number with + or -, from -1000 to 1000.
     181For [dmy] the number needs a unit after it: years, months, weeks, days, hours, minutes, seconds. Also "today", "yesterday" and "tomorrow" work.
     182Offsets follow the timezone of your site, the one in Settings > General.
    144183
    145184**Example**
    … …  
    148187[y offset="+1"] **2027** (if current year is 2026)
    149188[dmy offset="+1 year"] **12/04/2027**
     189[dmy offset="tomorrow"] **13/04/2026**
    150190
    151191
    … …  
    160200
    161201== Changelog ==
     202
     203= 2.6 =
     204* Offsets now follow the timezone set in your WordPress settings, on dates and on times.
     205* More consistent handling of the year format: [cyy] and friends with format="y" show a single year when there is nothing to range, same as format="Y" already did.
     206* Stricter check on the year attribute.
     207* [show_user_ip] reads forwarded headers only when the request comes through a known proxy. Local reverse proxies and Cloudflare work out of the box, other CDNs can be added with a filter, see the FAQ.
     208* Pages using [show_user_ip] are kept out of full page caching.
     209* Code revision on the date and copyright shortcodes.
     210* Tested with WordPress 7.0.
    162211
    163212= 2.5 =
    … …  
    269318== Upgrade Notice ==
    270319
     320= 2.6 =
     321Offsets now follow your site timezone. If your site sits behind a CDN other than Cloudflare, add it with the cys_trusted_proxies filter, otherwise [show_user_ip] shows the address of the CDN instead of the one of your visitor. See the FAQ.
     322
    271323= 2.5 =
    272324Security and accessibility improvements. Fixed negative offset bug. If you use the wpb_get_ip filter hook, please update to cys_get_ip (old hook still works for now).
    … …  
    364416= 0.1 =
    365417Initial release.
    366 
    367 `<?php code(); // goes in backticks ?>`
  • current-year-shortcode/trunk/shortcodes/copyright.php

    r3504475 r3646096  
    77 * License: GPLv2 or later
    88 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
     9 *
     10 * @package Current_Year_Shortcode
    911 */
    1012
    1113if ( ! defined( 'ABSPATH' ) ) {
    12         exit; // Exit if accessed directly
    13 }
    14 
    15 /**
    16  * Validate year parameter
    17  *
    18  * @param string $year The year to validate
    19  * @return array|false Array with 'display' (original format) and 'numeric' (4-digit) or false if invalid
     14        exit; // Exit if accessed directly.
     15}
     16
     17/**
     18 * Validate year parameter.
     19 *
     20 * @param string $year The year to validate.
     21 * @return array|false Array with 'display' (original format) and 'numeric' (4-digit) or false if invalid.
    2022 */
    2123function cys_validate_year( $year ) {
    22         // Check if year is numeric
    23         if ( ! is_numeric( $year ) ) {
     24        $year = trim( (string) $year );
     25
     26        /*
     27         * A plain run of one to four digits, nothing else. is_numeric() would also accept
     28         * "2003.7", "2e3", ".5" and "+2003", and the display value is printed verbatim:
     29         * those inputs used to reach the page as "© 2003.7-2026".
     30         */
     31        if ( ! preg_match( '/^\d{1,4}$/', $year ) ) {
    2432                return false;
    2533        }
    2634
    27         $original_year = $year; // Keep original format
    28         $year          = intval( $year );
    29 
    30         // Handle 2-digit years (00-99)
    31         if ( $year >= 0 && $year <= 99 ) {
    32                 // Convert 2-digit year to 4-digit year for comparison
    33                 // Years 00-30 are considered 2000-2030
    34                 // Years 31-99 are considered 1931-1999
    35                 if ( $year <= 30 ) {
    36                         $numeric_year = $year + 2000;
    37                 } else {
    38                         $numeric_year = $year + 1900;
     35        $original_year = $year; // Keep original format.
     36        $numeric_year  = (int) $year;
     37
     38        // Handle 2-digit years (00-99).
     39        if ( $numeric_year <= 99 ) {
     40                // Years 00-30 are considered 2000-2030, years 31-99 are considered 1931-1999.
     41                $numeric_year += ( $numeric_year <= 30 ) ? 2000 : 1900;
     42        }
     43
     44        /*
     45         * No range check is needed here: the pattern above caps the input at four digits,
     46         * so the normalized year is always between 100 and 9999.
     47         */
     48        return array(
     49                'display' => $original_year,  // Original format for display.
     50                'numeric' => $numeric_year,   // 4-digit for comparison.
     51        );
     52}
     53
     54/**
     55 * Wrap the copyright text in the symbol markup used by [cy] and [cyy].
     56 *
     57 * @param string $text The year or year range to display.
     58 * @return string The copyright symbol with the given text, escaped for safe output.
     59 */
     60function cys_present_copyright_symbol( $text ) {
     61        return '<span aria-label="' . esc_attr( 'Copyright ' . $text ) . '"><span aria-hidden="true">©</span> ' . esc_html( $text ) . '</span>';
     62}
     63
     64/**
     65 * Wrap the copyright text in the long form used by [cyyl].
     66 *
     67 * @param string $text The year or year range to display.
     68 * @return string The "Copyright" text with the given text, escaped for safe output.
     69 */
     70function cys_present_copyright_long( $text ) {
     71        return esc_html( 'Copyright ' . $text );
     72}
     73
     74/**
     75 * Wrap the copyright text in the long form with symbol used by [cyyls].
     76 *
     77 * @param string $text The year or year range to display.
     78 * @return string The copyright symbol and "Copyright" text, escaped for safe output.
     79 */
     80function cys_present_copyright_long_symbol( $text ) {
     81        return '<span aria-hidden="true">©</span> ' . esc_html( 'Copyright ' . $text );
     82}
     83
     84/**
     85 * Per-shortcode settings for the copyright shortcodes.
     86 *
     87 * @return array Map of shortcode tag to whether it takes a year attribute and which
     88 *               presenter renders its markup.
     89 */
     90function cys_copyright_shortcode_config() {
     91        return array(
     92                'cy'    => array(
     93                        'needs_year' => false,
     94                        'presenter'  => 'cys_present_copyright_symbol',
     95                ),
     96                'cyy'   => array(
     97                        'needs_year' => true,
     98                        'presenter'  => 'cys_present_copyright_symbol',
     99                ),
     100                'cyyl'  => array(
     101                        'needs_year' => true,
     102                        'presenter'  => 'cys_present_copyright_long',
     103                ),
     104                'cyyls' => array(
     105                        'needs_year' => true,
     106                        'presenter'  => 'cys_present_copyright_long_symbol',
     107                ),
     108        );
     109}
     110
     111/**
     112 * Shared renderer behind every copyright shortcode.
     113 *
     114 * @param array  $atts Shortcode attributes.
     115 * @param string $tag  Shortcode tag, used to look up the per-shortcode settings.
     116 * @return string The rendered copyright or an error message.
     117 */
     118function cys_render_copyright_shortcode( $atts, $tag ) {
     119        $config = cys_copyright_shortcode_config();
     120        $spec   = $config[ $tag ];
     121
     122        $defaults = array( 'format' => 'error' );
     123        if ( $spec['needs_year'] ) {
     124                $defaults = array(
     125                        'year'   => 'error enter first year, show guide',
     126                        'format' => 'error',
     127                );
     128        }
     129
     130        $atts = shortcode_atts( $defaults, $atts, $tag );
     131
     132        $atts = cys_sanitize_shortcode_atts( $atts );
     133
     134        // The year is validated before the format, so a bad year wins over a bad format.
     135        if ( $spec['needs_year'] ) {
     136                $validated_year = cys_validate_year( $atts['year'] );
     137                if ( false === $validated_year ) {
     138                        return cys_error( 'Invalid year value!' );
     139                }
     140
     141                $year_display = $validated_year['display'];  // Original format for display.
     142                $year_numeric = $validated_year['numeric'];  // 4-digit for comparison.
     143        }
     144
     145        if ( 'error' !== $atts['format'] ) {
     146                $format = cys_validate_date_format( $atts['format'], 'year' );
     147                if ( false === $format ) {
     148                        return cys_error( $atts['format'] . ' is not a valid year format!' );
    39149                }
    40150        } else {
    41                 $numeric_year = $year;
    42         }
    43 
    44         // Check if final year is within reasonable range (1-9999)
    45         if ( $numeric_year >= 1 && $numeric_year <= 9999 ) {
    46                 return array(
    47                         'display' => $original_year,  // Original format for display
    48                         'numeric' => $numeric_year,   // 4-digit for comparison
    49                 );
    50         }
    51 
    52         return false;
    53 }
    54 
    55 /**
    56  * Retrieve copyright symbol with current year
    57  *
    58  * @param array $atts Shortcode attributes
    59  *   - format (string) Optional. Year format (y or Y). Default: 'error' (uses default format Y)
    60  * @return string The copyright symbol with formatted year or error message
    61  */
     151                $format = 'Y';
     152        }
     153
     154        $current_year = date_i18n( $format );
     155
     156        if ( ! $spec['needs_year'] ) {
     157                return call_user_func( $spec['presenter'], $current_year );
     158        }
     159
     160        /*
     161         * The comparison runs on the canonical four-digit year, never on the formatted one:
     162         * date_i18n( 'y' ) returns two digits while the validated year is always four, so
     163         * with format="y" the two could never match and [cyy year="26" format="y"] used to
     164         * render "© 26-26" in the year 2026 instead of collapsing to a single year.
     165         */
     166        if ( (int) date_i18n( 'Y' ) === $year_numeric ) {
     167                return call_user_func( $spec['presenter'], $current_year );
     168        }
     169
     170        return call_user_func( $spec['presenter'], $year_display . '-' . $current_year );
     171}
     172
     173/**
     174 * Retrieve copyright symbol with current year.
     175 *
     176 * @param array $atts Shortcode attributes.
     177 *   - format (string) Optional. Year format (y or Y). Default: 'error' (uses default format Y).
     178 * @return string The copyright symbol with formatted year or error message.
     179 */
     180function cys_copy_year( $atts ) {
     181        return cys_render_copyright_shortcode( $atts, 'cy' );
     182}
    62183add_shortcode( 'cy', 'cys_copy_year' );
    63 function cys_copy_year( $atts ) {
    64         $atts = shortcode_atts(
    65                 array(
    66                         'format' => 'error',
    67                 ),
    68                 $atts,
    69                 'cy'
    70         );
    71 
    72         if ( 'error' !== $atts['format'] ) {
    73                 $validated_format = cys_validate_date_format( $atts['format'], 'year' );
    74                 if ( false !== $validated_format ) {
    75                         $year = date_i18n( $validated_format );
    76                         return '<span aria-label="' . esc_attr( 'Copyright ' . $year ) . '"><span aria-hidden="true">©</span> ' . esc_html( $year ) . '</span>';
    77                 } else {
    78                         return '<span role="alert">' . esc_html( sanitize_text_field( $atts['format'] ) . ' is not a valid year format!' ) . '</span>';
    79                 }
    80         } else {
    81                 $year = date_i18n( 'Y' );
    82                 return '<span aria-label="' . esc_attr( 'Copyright ' . $year ) . '"><span aria-hidden="true">©</span> ' . esc_html( $year ) . '</span>';
    83         }
    84 }
    85 
    86 /**
    87  * Retrieve copyright symbol with year range (first-year - last-year)
    88  *
    89  * @param array $atts Shortcode attributes
    90  *   - year (string) Required. The first year of copyright (1-9999, supports 2-digit years)
    91  *   - format (string) Optional. Year format (y or Y). Default: 'error' (uses default format Y)
    92  * @return string The copyright symbol with year range or error message
    93  */
     184
     185/**
     186 * Retrieve copyright symbol with year range (first-year - last-year).
     187 *
     188 * @param array $atts Shortcode attributes.
     189 *   - year (string) Required. The first year of copyright (1-9999, supports 2-digit years).
     190 *   - format (string) Optional. Year format (y or Y). Default: 'error' (uses default format Y).
     191 * @return string The copyright symbol with year range or error message.
     192 */
     193function cys_copy_year_year( $atts ) {
     194        return cys_render_copyright_shortcode( $atts, 'cyy' );
     195}
    94196add_shortcode( 'cyy', 'cys_copy_year_year' );
    95 function cys_copy_year_year( $atts ) {
    96         $atts = shortcode_atts(
    97                 array(
    98                         'year'   => 'error enter first year, show guide',
    99                         'format' => 'error',
    100                 ),
    101                 $atts,
    102                 'cyy'
    103         );
    104 
    105         // Validate year parameter
    106         $validated_year = cys_validate_year( $atts['year'] );
    107         if ( false === $validated_year ) {
    108                 return '<span role="alert">' . esc_html( 'Invalid year value!' ) . '</span>';
    109         }
    110 
    111         $year_display = $validated_year['display'];  // Original format for display
    112         $year_numeric = $validated_year['numeric'];  // 4-digit for comparison
    113 
    114         if ( 'error' !== $atts['format'] ) {
    115                 $validated_format = cys_validate_date_format( $atts['format'], 'year' );
    116                 if ( false !== $validated_format ) {
    117                         $current_year = date_i18n( $validated_format );
    118                         if ( (int) $current_year === $year_numeric ) {
    119                                 return '<span aria-label="' . esc_attr( 'Copyright ' . $current_year ) . '"><span aria-hidden="true">©</span> ' . esc_html( $current_year ) . '</span>';
    120                         } else {
    121                                 return '<span aria-label="' . esc_attr( 'Copyright ' . $year_display . '-' . $current_year ) . '"><span aria-hidden="true">©</span> ' . esc_html( $year_display . '-' . $current_year ) . '</span>';
    122                         }
    123                 } else {
    124                         return '<span role="alert">' . esc_html( sanitize_text_field( $atts['format'] ) . ' is not a valid year format!' ) . '</span>';
    125                 }
    126         } else {
    127                 $current_year = date_i18n( 'Y' );
    128                 if ( (int) $current_year === $year_numeric ) {
    129                         return '<span aria-label="' . esc_attr( 'Copyright ' . $current_year ) . '"><span aria-hidden="true">©</span> ' . esc_html( $current_year ) . '</span>';
    130                 } else {
    131                         return '<span aria-label="' . esc_attr( 'Copyright ' . $year_display . '-' . $current_year ) . '"><span aria-hidden="true">©</span> ' . esc_html( $year_display . '-' . $current_year ) . '</span>';
    132                 }
    133         }
    134 }
    135 
    136 /**
    137  * Retrieve "Copyright" text with year range (first-year - last-year)
    138  *
    139  * @param array $atts Shortcode attributes
    140  *   - year (string) Required. The first year of copyright (1-9999, supports 2-digit years)
    141  *   - format (string) Optional. Year format (y or Y). Default: 'error' (uses default format Y)
    142  * @return string The "Copyright" text with year range or error message
    143  */
     197
     198/**
     199 * Retrieve "Copyright" text with year range (first-year - last-year).
     200 *
     201 * @param array $atts Shortcode attributes.
     202 *   - year (string) Required. The first year of copyright (1-9999, supports 2-digit years).
     203 *   - format (string) Optional. Year format (y or Y). Default: 'error' (uses default format Y).
     204 * @return string The "Copyright" text with year range or error message.
     205 */
     206function cys_copy_year_year_long( $atts ) {
     207        return cys_render_copyright_shortcode( $atts, 'cyyl' );
     208}
    144209add_shortcode( 'cyyl', 'cys_copy_year_year_long' );
    145 function cys_copy_year_year_long( $atts ) {
    146         $atts = shortcode_atts(
    147                 array(
    148                         'year'   => 'error enter first year, show guide',
    149                         'format' => 'error',
    150                 ),
    151                 $atts,
    152                 'cyyl'
    153         );
    154 
    155         // Validate year parameter
    156         $validated_year = cys_validate_year( $atts['year'] );
    157         if ( false === $validated_year ) {
    158                 return '<span role="alert">' . esc_html( 'Invalid year value!' ) . '</span>';
    159         }
    160 
    161         $year_display = $validated_year['display'];  // Original format for display
    162         $year_numeric = $validated_year['numeric'];  // 4-digit for comparison
    163 
    164         if ( 'error' !== $atts['format'] ) {
    165                 $validated_format = cys_validate_date_format( $atts['format'], 'year' );
    166                 if ( false !== $validated_format ) {
    167                         $current_year = date_i18n( $validated_format );
    168                         if ( (int) $current_year === $year_numeric ) {
    169                                 return esc_html( 'Copyright ' . $current_year );
    170                         } else {
    171                                 return esc_html( 'Copyright ' . $year_display . '-' . $current_year );
    172                         }
    173                 } else {
    174                         return '<span role="alert">' . esc_html( sanitize_text_field( $atts['format'] ) . ' is not a valid year format!' ) . '</span>';
    175                 }
    176         } else {
    177                 $current_year = date_i18n( 'Y' );
    178                 if ( (int) $current_year === $year_numeric ) {
    179                         return esc_html( 'Copyright ' . $current_year );
    180                 } else {
    181                         return esc_html( 'Copyright ' . $year_display . '-' . $current_year );
    182                 }
    183         }
    184 }
    185 
    186 /**
    187  * Retrieve copyright symbol with "Copyright" text and year range (first-year - last-year)
    188  *
    189  * @param array $atts Shortcode attributes
    190  *   - year (string) Required. The first year of copyright (1-9999, supports 2-digit years)
    191  *   - format (string) Optional. Year format (y or Y). Default: 'error' (uses default format Y)
    192  * @return string The copyright symbol with "Copyright" text and year range or error message
    193  */
     210
     211/**
     212 * Retrieve copyright symbol with "Copyright" text and year range (first-year - last-year).
     213 *
     214 * @param array $atts Shortcode attributes.
     215 *   - year (string) Required. The first year of copyright (1-9999, supports 2-digit years).
     216 *   - format (string) Optional. Year format (y or Y). Default: 'error' (uses default format Y).
     217 * @return string The copyright symbol with "Copyright" text and year range or error message.
     218 */
     219function cys_copy_year_year_long_symbol( $atts ) {
     220        return cys_render_copyright_shortcode( $atts, 'cyyls' );
     221}
    194222add_shortcode( 'cyyls', 'cys_copy_year_year_long_symbol' );
    195 function cys_copy_year_year_long_symbol( $atts ) {
    196         $atts = shortcode_atts(
    197                 array(
    198                         'year'   => 'error enter first year, show guide',
    199                         'format' => 'error',
    200                 ),
    201                 $atts,
    202                 'cyyls'
    203         );
    204 
    205         // Validate year parameter
    206         $validated_year = cys_validate_year( $atts['year'] );
    207         if ( false === $validated_year ) {
    208                 return '<span role="alert">' . esc_html( 'Invalid year value!' ) . '</span>';
    209         }
    210 
    211         $year_display = $validated_year['display'];  // Original format for display
    212         $year_numeric = $validated_year['numeric'];  // 4-digit for comparison
    213 
    214         if ( 'error' !== $atts['format'] ) {
    215                 $validated_format = cys_validate_date_format( $atts['format'], 'year' );
    216                 if ( false !== $validated_format ) {
    217                         $current_year = date_i18n( $validated_format );
    218                         if ( (int) $current_year === $year_numeric ) {
    219                                 return '<span aria-hidden="true">©</span> ' . esc_html( 'Copyright ' . $current_year );
    220                         } else {
    221                                 return '<span aria-hidden="true">©</span> ' . esc_html( 'Copyright ' . $year_display . '-' . $current_year );
    222                         }
    223                 } else {
    224                         return '<span role="alert">' . esc_html( sanitize_text_field( $atts['format'] ) . ' is not a valid year format!' ) . '</span>';
    225                 }
    226         } else {
    227                 $current_year = date_i18n( 'Y' );
    228                 if ( (int) $current_year === $year_numeric ) {
    229                         return '<span aria-hidden="true">©</span> ' . esc_html( 'Copyright ' . $current_year );
    230                 } else {
    231                         return '<span aria-hidden="true">©</span> ' . esc_html( 'Copyright ' . $year_display . '-' . $current_year );
    232                 }
    233         }
    234 }
    235 
    236 /**
    237  * Retrieve copyright symbol (©)
    238  *
    239  * @param array $atts Shortcode attributes (not used)
    240  * @return string The copyright symbol, escaped for safe output
    241  */
     223
     224/**
     225 * Retrieve copyright symbol.
     226 *
     227 * @return string The copyright symbol, escaped for safe output.
     228 */
     229function cys_copy() {
     230        return '<abbr aria-label="' . esc_attr( 'Copyright' ) . '">©</abbr>';
     231}
    242232add_shortcode( 'c', 'cys_copy' );
    243 function cys_copy( $atts ) {
    244         return '<abbr aria-label="' . esc_attr( 'Copyright' ) . '">©</abbr>';
    245 }
    246 
    247 /**
    248  * Retrieve "Copyright" text
    249  *
    250  * @param array $atts Shortcode attributes (not used)
    251  * @return string The "Copyright" text, escaped for safe output
    252  */
     233
     234/**
     235 * Retrieve "Copyright" text.
     236 *
     237 * @return string The "Copyright" text, escaped for safe output.
     238 */
     239function cys_copylong() {
     240        return esc_html( 'Copyright' );
     241}
    253242add_shortcode( 'cc', 'cys_copylong' );
    254 function cys_copylong( $atts ) {
    255         return esc_html( 'Copyright' );
    256 }
  • current-year-shortcode/trunk/shortcodes/dates.php

    r3504475 r3646096  
    77 * License: GPLv2 or later
    88 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
     9 *
     10 * @package Current_Year_Shortcode
    911 */
    1012
    1113if ( ! defined( 'ABSPATH' ) ) {
    12         exit; // Exit if accessed directly
    13 }
    14 
    15 /**
    16  * Validate and sanitize offset parameter for date calculations
    17  *
    18  * @param string $offset The offset value to validate
    19  * @param string $type The type of offset (years, months, days, or generic)
    20  * @return string|false Sanitized offset or false if invalid
     14        exit; // Exit if accessed directly.
     15}
     16
     17/**
     18 * Validate and sanitize offset parameter for date calculations.
     19 *
     20 * @param string $offset The offset value to validate.
     21 * @param string $type   The type of offset. 'generic' also accepts strtotime keywords and a time
     22 *                       unit; any other value restricts the offset to a bare signed integer.
     23 * @return string|false Sanitized offset, 'none' when there is no offset, or false if invalid.
    2124 */
    2225function cys_validate_offset( $offset, $type = 'years' ) {
    23         // Remove any whitespace
     26        // Remove any whitespace.
    2427        $offset = trim( $offset );
    2528
    26         // Check if offset is empty or 'none'
    27         if ( empty( $offset ) || 'none' === $offset ) {
     29        // Check if offset is empty or 'none'.
     30        if ( '' === $offset || 'none' === $offset ) {
    2831                return 'none';
    2932        }
    3033
     34        /*
     35         * A run of digits long enough to overflow a float makes intval() answer 0, and a 0
     36         * sails straight through the range check further down: 309 nines would be accepted
     37         * as an offset and land on strtotime() as they are. The longest offset that means
     38         * anything here is something like "-1000 seconds".
     39         */
     40        if ( strlen( $offset ) > 32 ) {
     41                return false;
     42        }
     43
    3144        if ( 'generic' === $type ) {
    32                 // For generic strtotime, allow only safe patterns
    33                 // Pattern 1: today/yesterday/tomorrow
     45                // For generic strtotime, allow only safe patterns.
     46                // Pattern 1: today/yesterday/tomorrow.
    3447                if ( preg_match( '/^(today|yesterday|tomorrow)$/i', $offset ) ) {
    3548                        return $offset;
    3649                }
    3750
    38                 // Pattern 2: numeric offset with optional time unit
     51                // Pattern 2: numeric offset with optional time unit.
    3952                if ( preg_match( '/^([+-]?\s*\d+)(\s*(years?|months?|days?|weeks?|hours?|minutes?|seconds?))?$/i', $offset, $matches ) ) {
    4053                        $num_offset = intval( trim( $matches[1] ) );
    … …  
    4558
    4659                return false;
    47         } else {
    48                 // For specific types (years, months, days)
    49                 if ( preg_match( '/^[+-]?\s*\d+$/', $offset ) ) {
    50                         $num_offset = intval( trim( $offset ) );
    51                         // Extended range for more flexibility
    52                         if ( $num_offset >= -1000 && $num_offset <= 1000 ) {
    53                                 return $offset;
    54                         }
     60        }
     61
     62        // For specific types (years, months, days).
     63        if ( preg_match( '/^[+-]?\s*\d+$/', $offset ) ) {
     64                $num_offset = intval( trim( $offset ) );
     65                // Extended range for more flexibility.
     66                if ( $num_offset >= -1000 && $num_offset <= 1000 ) {
     67                        return $offset;
    5568                }
     69        }
     70
     71        return false;
     72}
     73
     74/**
     75 * Validate date format using flexible pattern-based approach.
     76 *
     77 * @param string $format The format to validate.
     78 * @param string $type   The type of format (year, month, day, date).
     79 * @return string|false Valid format or false if invalid.
     80 */
     81function cys_validate_date_format( $format, $type = 'year' ) {
     82        $format = trim( $format );
     83
     84        if ( '' === $format ) {
    5685                return false;
    5786        }
    58 }
    59 
    60 /**
    61  * Validate date format using flexible pattern-based approach
    62  *
    63  * @param string $format The format to validate
    64  * @param string $type The type of format (year, month, day, date)
    65  * @return string|false Valid format or false if invalid
    66  */
    67 function cys_validate_date_format( $format, $type = 'year' ) {
    68         // Remove any whitespace
    69         $format = trim( $format );
    70 
    71         // Check if format is empty
    72         if ( empty( $format ) ) {
     87
     88        /*
     89         * The patterns below are anchored single character classes, so they cannot backtrack
     90         * catastrophically; the cap is here for output size instead. Every accepted character
     91         * expands to at least one character of date, so a format of a hundred thousand Y would
     92         * render a hundred thousand digits into the page. No real date format is this long.
     93         */
     94        if ( strlen( $format ) > 32 ) {
    7395                return false;
    7496        }
    7597
    76         // Define allowed format characters based on type
     98        // Define allowed format characters based on type.
    7799        $allowed_chars = array(
    78100                'year'  => '/^[yY]+$/',
    … …  
    82104        );
    83105
    84         // Check if format matches allowed pattern for the type
     106        // Check if format matches allowed pattern for the type.
    85107        if ( isset( $allowed_chars[ $type ] ) && preg_match( $allowed_chars[ $type ], $format ) ) {
    86108                return $format;
    … …  
    91113
    92114/**
    93  * Sanitize shortcode attributes for dates
    94  *
    95  * @param array $atts The attributes to sanitize
    96  * @return array Sanitized attributes
    97  */
    98 function cys_sanitize_date_atts( $atts ) {
    99         $sanitized = array();
    100 
    101         foreach ( $atts as $key => $value ) {
    102                 // Sanitize text fields
    103                 if ( in_array( $key, array( 'format', 'offset' ), true ) ) {
    104                         $sanitized[ $key ] = sanitize_text_field( $value );
    105                 } else {
    106                         $sanitized[ $key ] = $value;
    107                 }
    108         }
    109 
    110         return $sanitized;
    111 }
    112 
    113 /**
    114  * Retrieve current year with optional format and offset
    115  *
    116  * @param array $atts Shortcode attributes
    117  *   - format (string) Optional. Year format (y or Y). Default: 'error' (uses default format)
    118  *   - offset (string) Optional. Year offset (+1, -1, etc.). Default: 'none'
    119  * @return string The formatted year or error message
    120  */
    121 add_shortcode( 'y', 'cys_year' );
    122 function cys_year( $atts ) {
     115 * Sanitize every attribute of a shortcode.
     116 *
     117 * Keys are not enumerated on purpose: a shortcode_atts_{$tag} filter can add keys this
     118 * plugin was never written for, and sanitize_text_field() answers '' for arrays and
     119 * objects by itself, so passing everything through it is both shorter and safer than
     120 * listing the expected names.
     121 *
     122 * @param array $atts The attributes to sanitize.
     123 * @return array Sanitized attributes.
     124 */
     125function cys_sanitize_shortcode_atts( $atts ) {
     126        return array_map( 'sanitize_text_field', (array) $atts );
     127}
     128
     129/**
     130 * Wrap an error message in the markup every shortcode uses to report one.
     131 *
     132 * @param string $message The message to display.
     133 * @return string The message, escaped for safe output.
     134 */
     135function cys_error( $message ) {
     136        return '<span role="alert">' . esc_html( $message ) . '</span>';
     137}
     138
     139/**
     140 * Format a date placed at a relative offset from the site's own wall clock.
     141 *
     142 * Relative expressions are resolved by strtotime() against the UTC timezone WordPress forces
     143 * in wp-settings.php, and date_i18n() then reinterprets that UTC wall clock as local time.
     144 * The two do not cancel out: every site east or west of UTC shows the wrong day during
     145 * part of each day, and the wrong year around New Year. Anchoring the expression to a
     146 * local DateTimeImmutable and rendering the resulting instant with wp_date() keeps the
     147 * day right in every timezone.
     148 *
     149 * @param string $format     Date format, already validated by cys_validate_date_format().
     150 * @param string $expression Relative expression, already validated by cys_validate_offset().
     151 * @return string The formatted date.
     152 */
     153function cys_format_offset_date( $format, $expression ) {
     154        /*
     155         * current_datetime() and wp_date() both arrived in WordPress 5.3. On older versions
     156         * keep the historical behaviour rather than fataling: wrong day near midnight, but
     157         * exactly as wrong as every previous release of this plugin.
     158         */
     159        if ( ! function_exists( 'current_datetime' ) || ! function_exists( 'wp_date' ) ) {
     160                return date_i18n( $format, strtotime( $expression ) );
     161        }
     162
     163        $now = current_datetime();
     164
     165        /*
     166         * An offset carrying no time unit, such as "5" on [dmy], is not a relative expression
     167         * PHP can parse. strtotime() used to answer false in silence, while modify() emits a
     168         * warning below PHP 8.3 and throws from 8.3 on, so the unparsable case is recognized
     169         * here and rendered as the current moment: what every previous release displayed.
     170         */
     171        if ( false === strtotime( $expression, $now->getTimestamp() ) ) {
     172                return wp_date( $format, $now->getTimestamp() );
     173        }
     174
     175        try {
     176                $moment = $now->modify( $expression );
     177        } catch ( Exception $e ) {
     178                // Backstop: reached only if modify() rejects an expression strtotime() accepted.
     179                $moment = $now;
     180        }
     181
     182        return wp_date( $format, $moment->getTimestamp() );
     183}
     184
     185/**
     186 * Per-shortcode settings for the date shortcodes.
     187 *
     188 * The offset_type doubles as the time unit appended to a numeric offset, so the two can
     189 * never drift apart; 'generic' means the offset is a whole strtotime expression and gets
     190 * no unit. The format_type selects the allowed characters in cys_validate_date_format()
     191 * and also names the shortcode in its error message.
     192 *
     193 * @return array Map of shortcode tag to its offset type, format type and default format.
     194 */
     195function cys_date_shortcode_config() {
     196        return array(
     197                'y'   => array(
     198                        'offset_type'    => 'years',
     199                        'format_type'    => 'year',
     200                        'default_format' => 'Y',
     201                ),
     202                'm'   => array(
     203                        'offset_type'    => 'months',
     204                        'format_type'    => 'month',
     205                        'default_format' => 'F',
     206                ),
     207                'd'   => array(
     208                        'offset_type'    => 'days',
     209                        'format_type'    => 'day',
     210                        'default_format' => 'd',
     211                ),
     212                'dmy' => array(
     213                        'offset_type'    => 'generic',
     214                        'format_type'    => 'date',
     215                        'default_format' => 'd/m/Y',
     216                ),
     217        );
     218}
     219
     220/**
     221 * Shared renderer behind every date shortcode.
     222 *
     223 * @param array  $atts Shortcode attributes.
     224 * @param string $tag  Shortcode tag, used to look up the per-shortcode settings.
     225 * @return string The formatted date or an error message.
     226 */
     227function cys_render_date_shortcode( $atts, $tag ) {
     228        $config = cys_date_shortcode_config();
     229        $spec   = $config[ $tag ];
     230
    123231        $atts = shortcode_atts(
    124232                array(
    … …  
    127235                ),
    128236                $atts,
    129                 'y'
     237                $tag
    130238        );
    131239
    132         // Sanitize shortcode attributes
    133         $atts = cys_sanitize_date_atts( $atts );
    134 
    135         // Validate offset parameter
    136         $validated_offset = cys_validate_offset( $atts['offset'], 'years' );
     240        $atts = cys_sanitize_shortcode_atts( $atts );
     241
     242        $validated_offset = cys_validate_offset( $atts['offset'], $spec['offset_type'] );
    137243        if ( false === $validated_offset ) {
    138                 return '<span role="alert">' . esc_html( 'Invalid offset value!' ) . '</span>';
     244                return cys_error( 'Invalid offset value!' );
    139245        }
    140246
    141247        if ( 'error' !== $atts['format'] ) {
    142                 $validated_format = cys_validate_date_format( $atts['format'], 'year' );
    143                 if ( false !== $validated_format ) {
    144                         if ( 'none' !== $validated_offset ) {
    145                                 return esc_html( date_i18n( $validated_format, strtotime( $validated_offset . ' years' ) ) );
    146                         } else {
    147                                 return esc_html( date_i18n( $validated_format ) );
    148                         }
    149                 } else {
    150                         return '<span role="alert">' . esc_html( sanitize_text_field( $atts['format'] ) . ' is not a valid year format!' ) . '</span>';
     248                $format = cys_validate_date_format( $atts['format'], $spec['format_type'] );
     249                if ( false === $format ) {
     250                        return cys_error( $atts['format'] . ' is not a valid ' . $spec['format_type'] . ' format!' );
    151251                }
    152252        } else {
    153                 if ( 'none' !== $validated_offset ) {
    154                         return esc_html( date_i18n( 'Y', strtotime( $validated_offset . ' years' ) ) );
    155                 } else {
    156                         return esc_html( date_i18n( 'Y' ) );
    157                 }
    158         }
    159 }
    160 
    161 /**
    162  * Retrieve current month with optional format and offset
    163  *
    164  * @param array $atts Shortcode attributes
    165  *   - format (string) Optional. Month format (F, m, M, n). Default: 'error' (uses default format)
    166  *   - offset (string) Optional. Month offset (+1, -1, etc.). Default: 'none'
    167  * @return string The formatted month or error message
    168  */
     253                $format = $spec['default_format'];
     254        }
     255
     256        if ( 'none' === $validated_offset ) {
     257                return esc_html( date_i18n( $format ) );
     258        }
     259
     260        // 'generic' offsets are whole expressions already; the others are bare numbers needing their unit.
     261        $unit = ( 'generic' === $spec['offset_type'] ) ? '' : ' ' . $spec['offset_type'];
     262
     263        return esc_html( cys_format_offset_date( $format, $validated_offset . $unit ) );
     264}
     265
     266/**
     267 * Retrieve current year with optional format and offset.
     268 *
     269 * @param array $atts Shortcode attributes.
     270 *   - format (string) Optional. Year format (y or Y). Default: 'error' (uses default format).
     271 *   - offset (string) Optional. Year offset (+1, -1, etc.). Default: 'none'.
     272 * @return string The formatted year or error message.
     273 */
     274function cys_year( $atts ) {
     275        return cys_render_date_shortcode( $atts, 'y' );
     276}
     277add_shortcode( 'y', 'cys_year' );
     278
     279/**
     280 * Retrieve current month with optional format and offset.
     281 *
     282 * @param array $atts Shortcode attributes.
     283 *   - format (string) Optional. Month format (F, m, M, n). Default: 'error' (uses default format).
     284 *   - offset (string) Optional. Month offset (+1, -1, etc.). Default: 'none'.
     285 * @return string The formatted month or error message.
     286 */
     287function cys_month( $atts ) {
     288        return cys_render_date_shortcode( $atts, 'm' );
     289}
    169290add_shortcode( 'm', 'cys_month' );
    170 function cys_month( $atts ) {
    171         $atts = shortcode_atts(
    172                 array(
    173                         'format' => 'error',
    174                         'offset' => 'none',
    175                 ),
    176                 $atts,
    177                 'm'
    178         );
    179 
    180         // Sanitize shortcode attributes
    181         $atts = cys_sanitize_date_atts( $atts );
    182 
    183         // Validate offset parameter
    184         $validated_offset = cys_validate_offset( $atts['offset'], 'months' );
    185         if ( false === $validated_offset ) {
    186                 return '<span role="alert">' . esc_html( 'Invalid offset value!' ) . '</span>';
    187         }
    188 
    189         if ( 'error' !== $atts['format'] ) {
    190                 $validated_format = cys_validate_date_format( $atts['format'], 'month' );
    191                 if ( false !== $validated_format ) {
    192                         if ( 'none' !== $validated_offset ) {
    193                                 return esc_html( date_i18n( $validated_format, strtotime( $validated_offset . ' months' ) ) );
    194                         } else {
    195                                 return esc_html( date_i18n( $validated_format ) );
    196                         }
    197                 } else {
    198                         return '<span role="alert">' . esc_html( sanitize_text_field( $atts['format'] ) . ' is not a valid month format!' ) . '</span>';
    199                 }
    200         } else {
    201                 if ( 'none' !== $validated_offset ) {
    202                         return esc_html( date_i18n( 'F', strtotime( $validated_offset . ' months' ) ) );
    203                 } else {
    204                         return esc_html( date_i18n( 'F' ) );
    205                 }
    206         }
    207 }
    208 
    209 /**
    210  * Retrieve current day with optional format and offset
    211  *
    212  * @param array $atts Shortcode attributes
    213  *   - format (string) Optional. Day format (d, D, j, N, S, w, z, t). Default: 'error' (uses default format)
    214  *   - offset (string) Optional. Day offset (+1, -1, etc.). Default: 'none'
    215  * @return string The formatted day or error message
    216  */
     291
     292/**
     293 * Retrieve current day with optional format and offset.
     294 *
     295 * @param array $atts Shortcode attributes.
     296 *   - format (string) Optional. Day format (d, D, j, N, S, w, z, t). Default: 'error' (uses default format).
     297 *   - offset (string) Optional. Day offset (+1, -1, etc.). Default: 'none'.
     298 * @return string The formatted day or error message.
     299 */
     300function cys_day( $atts ) {
     301        return cys_render_date_shortcode( $atts, 'd' );
     302}
    217303add_shortcode( 'd', 'cys_day' );
    218 function cys_day( $atts ) {
    219         $atts = shortcode_atts(
    220                 array(
    221                         'format' => 'error',
    222                         'offset' => 'none',
    223                 ),
    224                 $atts,
    225                 'd'
    226         );
    227 
    228         // Sanitize shortcode attributes
    229         $atts = cys_sanitize_date_atts( $atts );
    230 
    231         // Validate offset parameter
    232         $validated_offset = cys_validate_offset( $atts['offset'], 'days' );
    233         if ( false === $validated_offset ) {
    234                 return '<span role="alert">' . esc_html( 'Invalid offset value!' ) . '</span>';
    235         }
    236 
    237         if ( 'error' !== $atts['format'] ) {
    238                 $validated_format = cys_validate_date_format( $atts['format'], 'day' );
    239                 if ( false !== $validated_format ) {
    240                         if ( 'none' !== $validated_offset ) {
    241                                 return esc_html( date_i18n( $validated_format, strtotime( $validated_offset . ' days' ) ) );
    242                         } else {
    243                                 return esc_html( date_i18n( $validated_format ) );
    244                         }
    245                 } else {
    246                         return '<span role="alert">' . esc_html( sanitize_text_field( $atts['format'] ) . ' is not a valid day format!' ) . '</span>';
    247                 }
    248         } else {
    249                 if ( 'none' !== $validated_offset ) {
    250                         return esc_html( date_i18n( 'd', strtotime( $validated_offset . ' days' ) ) );
    251                 } else {
    252                         return esc_html( date_i18n( 'd' ) );
    253                 }
    254         }
    255 }
    256 
    257 /**
    258  * Retrieve current date with optional format and offset
    259  *
    260  * @param array $atts Shortcode attributes
    261  *   - format (string) Optional. Date format (all PHP date format characters). Default: 'error' (uses d/m/Y)
    262  *   - offset (string) Optional. Date offset (+1 year, +5 months, today, yesterday, tomorrow, etc.). Default: 'none'
    263  * @return string The formatted date or error message
    264  */
     304
     305/**
     306 * Retrieve current date with optional format and offset.
     307 *
     308 * @param array $atts Shortcode attributes.
     309 *   - format (string) Optional. Date format (all PHP date format characters). Default: 'error' (uses d/m/Y).
     310 *   - offset (string) Optional. Date offset (+1 year, +5 months, today, yesterday, tomorrow, etc.). Default: 'none'.
     311 * @return string The formatted date or error message.
     312 */
     313function cys_current_date( $atts ) {
     314        return cys_render_date_shortcode( $atts, 'dmy' );
     315}
    265316add_shortcode( 'dmy', 'cys_current_date' );
    266 function cys_current_date( $atts ) {
    267         $atts = shortcode_atts(
    268                 array(
    269                         'format' => 'error',
    270                         'offset' => 'none',
    271                 ),
    272                 $atts,
    273                 'dmy'
    274         );
    275 
    276         // Sanitize shortcode attributes
    277         $atts = cys_sanitize_date_atts( $atts );
    278 
    279         // Validate offset parameter for generic strtotime
    280         $validated_offset = cys_validate_offset( $atts['offset'], 'generic' );
    281         if ( false === $validated_offset ) {
    282                 return '<span role="alert">' . esc_html( 'Invalid offset value!' ) . '</span>';
    283         }
    284 
    285         if ( 'error' !== $atts['format'] ) {
    286                 $validated_format = cys_validate_date_format( $atts['format'], 'date' );
    287                 if ( false !== $validated_format ) {
    288                         if ( 'none' !== $validated_offset ) {
    289                                 return esc_html( date_i18n( $validated_format, strtotime( $validated_offset ) ) );
    290                         } else {
    291                                 return esc_html( date_i18n( $validated_format ) );
    292                         }
    293                 } else {
    294                         return '<span role="alert">' . esc_html( sanitize_text_field( $atts['format'] ) . ' is not a valid date format!' ) . '</span>';
    295                 }
    296         } else {
    297                 if ( 'none' !== $validated_offset ) {
    298                         return esc_html( date_i18n( 'd/m/Y', strtotime( $validated_offset ) ) );
    299                 } else {
    300                         return esc_html( date_i18n( 'd/m/Y' ) );
    301                 }
    302         }
    303 }
  • current-year-shortcode/trunk/shortcodes/index.php

    r2835678 r3646096  
    11<?php
    2 // Silence is golden.
     2/**
     3 * Silence is golden.
     4 *
     5 * @package Current_Year_Shortcode
     6 */
  • current-year-shortcode/trunk/shortcodes/ip.php

    r3504475 r3646096  
    77 * License: GPLv2 or later
    88 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
     9 *
     10 * @package Current_Year_Shortcode
    911 */
    1012
    1113if ( ! defined( 'ABSPATH' ) ) {
    12         exit; // Exit if accessed directly
    13 }
    14 
    15 /**
    16  * Retrieve user IP address with security validation and sanitization
    17  *
    18  * Checks multiple proxy headers in priority order and validates IP addresses.
    19  * Falls back to REMOTE_ADDR if no valid IP is found in proxy headers.
     14        exit; // Exit if accessed directly.
     15}
     16
     17/**
     18 * The networks Cloudflare serves requests from.
     19 *
     20 * Published at https://www.cloudflare.com/ips/ and read on 2026-08-12. Kept apart from
     21 * the other trusted networks because the CF-Connecting-IP header means nothing unless
     22 * the request really arrived from Cloudflare. If this list ever falls behind, a site on
     23 * a new range shows the address of the Cloudflare edge instead of the visitor: a wrong
     24 * answer, never a forgeable one.
     25 *
     26 * @return array List of CIDR ranges.
     27 */
     28function cys_cloudflare_ranges() {
     29        return array(
     30                // IPv4.
     31                '173.245.48.0/20',
     32                '103.21.244.0/22',
     33                '103.22.200.0/22',
     34                '103.31.4.0/22',
     35                '141.101.64.0/18',
     36                '108.162.192.0/18',
     37                '190.93.240.0/20',
     38                '188.114.96.0/20',
     39                '197.234.240.0/22',
     40                '198.41.128.0/17',
     41                '162.158.0.0/15',
     42                '104.16.0.0/13',
     43                '104.24.0.0/14',
     44                '172.64.0.0/13',
     45                '131.0.72.0/22',
     46                // IPv6.
     47                '2400:cb00::/32',
     48                '2606:4700::/32',
     49                '2803:f800::/32',
     50                '2405:b500::/32',
     51                '2405:8100::/32',
     52                '2a06:98c0::/29',
     53                '2c0f:f248::/32',
     54        );
     55}
     56
     57/**
     58 * Public networks whose forwarded headers can be believed.
     59 *
     60 * A forwarded header is only as trustworthy as whoever set it, and the one party a site
     61 * can identify with certainty is the one that opened the TCP connection: REMOTE_ADDR. So
     62 * the question is never "which header do I read" but "did this request reach me through
     63 * a proxy of mine". It is the rule Apache's mod_remoteip applies with RemoteIPTrustedProxy.
     64 *
     65 * Private and reserved networks are recognized separately, in cys_is_trusted_proxy().
     66 * Any other CDN or load balancer with a public address is declared from the site:
     67 *
     68 *     add_filter( 'cys_trusted_proxies', function ( $proxies ) {
     69 *         $proxies[] = '203.0.113.0/24';
     70 *         return $proxies;
     71 *     } );
     72 *
     73 * @return array List of CIDR ranges and single addresses.
     74 */
     75function cys_trusted_proxies() {
     76        $proxies = apply_filters( 'cys_trusted_proxies', cys_cloudflare_ranges() );
     77
     78        /*
     79         * A callback that forgets its return statement hands back null, which would silently
     80         * drop the shipped ranges along with whatever it meant to add.
     81         */
     82        return is_array( $proxies ) ? $proxies : cys_cloudflare_ranges();
     83}
     84
     85/**
     86 * Reduce an IPv4-mapped IPv6 address to its plain IPv4 form.
     87 *
     88 * A web server listening on a dual-stack socket reports an IPv4 peer as ::ffff:127.0.0.1,
     89 * which matches no IPv4 range and would quietly switch the whole mechanism off.
     90 *
     91 * @param string $ip The address to normalize.
     92 * @return string The IPv4 address when the input was a mapped one, the input otherwise.
     93 */
     94function cys_normalize_ip( $ip ) {
     95        $packed = inet_pton( $ip );
     96
     97        /*
     98         * Recognising the mapping on the text would mean chasing every legal way of writing it:
     99         * ::ffff:8.8.8.8 and ::ffff:0808:0808 are the same address, and a check that only knows
     100         * the dotted form leaves the other one looking like a plain IPv6 address. All of them
     101         * collapse to the same sixteen bytes, so the binary form is the one place to look.
     102         */
     103        if ( false !== $packed && 16 === strlen( $packed ) && "\0\0\0\0\0\0\0\0\0\0\xff\xff" === substr( $packed, 0, 12 ) ) {
     104                $unmapped = inet_ntop( substr( $packed, 12, 4 ) );
     105
     106                if ( false !== $unmapped ) {
     107                        return $unmapped;
     108                }
     109        }
     110
     111        return $ip;
     112}
     113
     114/**
     115 * Tell whether an address falls inside a CIDR range.
     116 *
     117 * Works on both IPv4 and IPv6 by masking the packed binary forms, so an IPv4 address is
     118 * never matched against an IPv6 range or the other way round.
     119 *
     120 * @param string $ip    The address to test.
     121 * @param string $range A CIDR range, or a single address when it carries no slash.
     122 * @return bool True when the address is inside the range.
     123 */
     124function cys_ip_in_range( $ip, $range ) {
     125        if ( false === strpos( $range, '/' ) ) {
     126                return $ip === $range;
     127        }
     128
     129        list( $subnet, $prefix ) = explode( '/', $range, 2 );
     130
     131        $ip_packed     = inet_pton( $ip );
     132        $subnet_packed = inet_pton( $subnet );
     133
     134        if ( false === $ip_packed || false === $subnet_packed || strlen( $ip_packed ) !== strlen( $subnet_packed ) ) {
     135                return false;
     136        }
     137
     138        /*
     139         * This list is extended from the site, so a typo has to fail closed: '10.0.0.0/' and
     140         * '10.0.0.0/abc' both cast to a prefix of 0, and a prefix of 0 matches every address
     141         * there is. Turning one mistyped range into "trust the whole internet" is exactly
     142         * what this function exists to prevent.
     143         */
     144        if ( ! ctype_digit( $prefix ) || (int) $prefix > strlen( $ip_packed ) * 8 ) {
     145                return false;
     146        }
     147
     148        $prefix     = (int) $prefix;
     149        $full_bytes = (int) ( $prefix / 8 );
     150        $spare_bits = $prefix % 8;
     151
     152        $mask = str_repeat( "\xFF", $full_bytes );
     153        if ( $spare_bits > 0 ) {
     154                $mask .= chr( ( 0xFF << ( 8 - $spare_bits ) ) & 0xFF );
     155        }
     156        $mask = str_pad( $mask, strlen( $ip_packed ), "\x00" );
     157
     158        return ( $ip_packed & $mask ) === ( $subnet_packed & $mask );
     159}
     160
     161/**
     162 * Tell whether an address falls inside any of the given ranges.
     163 *
     164 * @param string $ip     The address to test.
     165 * @param array  $ranges List of CIDR ranges.
     166 * @return bool True when the address is inside one of them.
     167 */
     168function cys_ip_in_any_range( $ip, $ranges ) {
     169        foreach ( $ranges as $range ) {
     170                if ( is_string( $range ) && cys_ip_in_range( $ip, $range ) ) {
     171                        return true;
     172                }
     173        }
     174
     175        return false;
     176}
     177
     178/**
     179 * Tell whether an address belongs to one of the site's own proxies.
     180 *
     181 * @param string $ip The address to test.
     182 * @return bool True when the address is a trusted proxy.
     183 */
     184function cys_is_trusted_proxy( $ip ) {
     185        // Normalized here too, so the answer does not depend on which form the caller passed in.
     186        $ip = cys_normalize_ip( $ip );
     187
     188        if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
     189                return false;
     190        }
     191
     192        /*
     193         * A peer on a private or reserved network did not reach the site across the public
     194         * internet: on the overwhelming majority of installations it is infrastructure of the
     195         * site itself, a reverse proxy, a load balancer or a container gateway.
     196         *
     197         * The trade-off is deliberate. mod_remoteip ships no defaults at all and makes the
     198         * administrator name every proxy, which is stricter but leaves the common
     199         * nginx-to-PHP-FPM setup reporting 127.0.0.1 for every visitor until someone
     200         * configures it. The cost of trusting these networks is that on a site also reachable
     201         * from an internal LAN, someone already inside that network can dictate the address
     202         * shown back to them by sending their own X-Forwarded-For.
     203         */
     204        if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
     205                return true;
     206        }
     207
     208        return cys_ip_in_any_range( $ip, cys_trusted_proxies() );
     209}
     210
     211/**
     212 * Read the address a trusted proxy forwarded.
     213 *
     214 * Cloudflare states a single address in CF-Connecting-IP, so that one is read as is, but
     215 * only from a request that really arrived from Cloudflare: any other proxy would simply
     216 * be handing on a header the visitor wrote.
     217 *
     218 * X-Forwarded-For is instead a chain, "client, proxy1, proxy2", to which every hop
     219 * APPENDS the address it received the connection from. Nothing stops a visitor from
     220 * sending a chain of their own, which the first proxy then appends to, so the leftmost
     221 * entry is whatever the visitor decided to put there: reading it hands the answer back
     222 * to the party being identified. The chain is walked from the right instead, stepping
     223 * over the site's own proxies, and the first address that is not one of them is the
     224 * furthest point a proxy of ours actually observed.
     225 *
     226 * @param string $remote_addr The address that opened the connection.
     227 * @return string The forwarded address, or an empty string when there is none to trust.
     228 */
     229function cys_forwarded_client_ip( $remote_addr ) {
     230        if ( ! empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) && cys_ip_in_any_range( $remote_addr, cys_cloudflare_ranges() ) ) {
     231                $forwarded = cys_normalize_ip( sanitize_text_field( wp_unslash( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) );
     232
     233                if ( filter_var( $forwarded, FILTER_VALIDATE_IP ) ) {
     234                        return $forwarded;
     235                }
     236        }
     237
     238        if ( empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
     239                return '';
     240        }
     241
     242        $chain = explode( ',', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) );
     243
     244        foreach ( array_reverse( $chain ) as $hop ) {
     245                $hop = cys_normalize_ip( trim( $hop ) );
     246
     247                if ( ! filter_var( $hop, FILTER_VALIDATE_IP ) ) {
     248                        return ''; // A malformed hop breaks the chain of custody: stop believing the rest of it.
     249                }
     250
     251                if ( ! cys_is_trusted_proxy( $hop ) ) {
     252                        return $hop;
     253                }
     254        }
     255
     256        return '';
     257}
     258
     259/**
     260 * Resolve the address of whoever is asking for the page.
     261 *
     262 * @return string A validated IP address, or an empty string when none can be established.
     263 */
     264function cys_get_client_ip() {
     265        if ( empty( $_SERVER['REMOTE_ADDR'] ) ) {
     266                return '';
     267        }
     268
     269        // Unslash and sanitize the IP address from $_SERVER.
     270        $remote_addr = cys_normalize_ip( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) );
     271
     272        if ( ! filter_var( $remote_addr, FILTER_VALIDATE_IP ) ) {
     273                return '';
     274        }
     275
     276        /*
     277         * Forwarded headers are read only when the request actually arrived through one of
     278         * the site's proxies. On a direct connection they are just text the visitor typed.
     279         */
     280        if ( ! cys_is_trusted_proxy( $remote_addr ) ) {
     281                return $remote_addr;
     282        }
     283
     284        $forwarded = cys_forwarded_client_ip( $remote_addr );
     285
     286        return ( '' !== $forwarded ) ? $forwarded : $remote_addr;
     287}
     288
     289/**
     290 * Retrieve user IP address with security validation and sanitization.
     291 *
    20292 * Returns '0.0.0.0' as a safe default if no valid IP is found.
    21293 *
    … …  
    23295 * Site administrators should ensure proper privacy policy disclosure.
    24296 *
    25  * @return string The user's IP address, escaped for safe output
    26  */
     297 * @return string The user's IP address, escaped for safe output.
     298 */
     299function cys_retrieve_ip() {
     300        /*
     301         * The output differs for every visitor, so the page must not be stored in a full page
     302         * cache: one visitor's address would be served to everybody after them. DONOTCACHEPAGE
     303         * is the constant the WordPress caching plugins look for and is what actually does the
     304         * work here. The nocache_headers() call is a second line of defence for the rare case
     305         * where output has not started yet, since it returns without doing anything once the
     306         * headers have been sent, which is the normal state of affairs by the time a shortcode
     307         * runs. Neither of the two reaches a cache sitting in front of the web server, so a
     308         * site with "cache everything" enabled at the edge has to exclude the page there too.
     309         */
     310        if ( ! defined( 'DONOTCACHEPAGE' ) ) {
     311                define( 'DONOTCACHEPAGE', true );
     312        }
     313        nocache_headers();
     314
     315        $ip = cys_get_client_ip();
     316
     317        // Default safe value if no valid IP found.
     318        if ( '' === $ip ) {
     319                $ip = '0.0.0.0';
     320        }
     321
     322        // Backward-compatible deprecated hook — fires only if someone is using it.
     323        $ip = apply_filters_deprecated( 'wpb_get_ip', array( $ip ), '2.5', 'cys_get_ip' );
     324
     325        // Escape output for security and apply filters.
     326        return esc_html( apply_filters( 'cys_get_ip', $ip ) );
     327}
    27328add_shortcode( 'show_user_ip', 'cys_retrieve_ip' );
    28 function cys_retrieve_ip() {
    29         $ip = '';
    30 
    31         // List of proxy headers to check (in priority order)
    32         $proxy_headers = array(
    33                 'HTTP_CF_CONNECTING_IP',     // Cloudflare
    34                 'HTTP_CLIENT_IP',            // Proxy
    35                 'HTTP_X_FORWARDED_FOR',      // Load balancer/proxy
    36                 'HTTP_X_FORWARDED',          // Proxy
    37                 'HTTP_X_CLUSTER_CLIENT_IP',  // Cluster
    38                 'HTTP_FORWARDED_FOR',        // Proxy
    39                 'HTTP_FORWARDED',            // Proxy
    40                 'REMOTE_ADDR'                // Standard
    41         );
    42 
    43         // Find the first valid IP address
    44         foreach ( $proxy_headers as $header ) {
    45                 if ( ! empty( $_SERVER[ $header ] ) ) {
    46                         // Unslash and sanitize the IP address from $_SERVER
    47                         $candidate_ip = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );
    48 
    49                         // If multiple IPs (comma separated), take the first one
    50                         if ( false !== strpos( $candidate_ip, ',' ) ) {
    51                                 $parts        = explode( ',', $candidate_ip );
    52                                 $candidate_ip = trim( $parts[0] );
    53                         }
    54 
    55                         // Validate the IP address
    56                         if ( filter_var( $candidate_ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
    57                                 $ip = $candidate_ip;
    58                                 break;
    59                         }
    60                 }
    61         }
    62 
    63         // Fallback to REMOTE_ADDR if no valid IP found
    64         if ( empty( $ip ) && ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
    65                 // Unslash and sanitize the IP address from $_SERVER
    66                 $fallback_ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
    67                 if ( filter_var( $fallback_ip, FILTER_VALIDATE_IP ) ) {
    68                         $ip = $fallback_ip;
    69                 }
    70         }
    71 
    72         // Default safe value if no valid IP found
    73         if ( empty( $ip ) ) {
    74                 $ip = '0.0.0.0';
    75         }
    76 
    77         // Backward-compatible deprecated hook — fires only if someone is using it
    78         $ip = apply_filters_deprecated( 'wpb_get_ip', array( $ip ), '2.5', 'cys_get_ip' );
    79 
    80         // Escape output for security and apply filters
    81         return esc_html( apply_filters( 'cys_get_ip', $ip ) );
    82 }
  • current-year-shortcode/trunk/shortcodes/symbols.php

    r3504475 r3646096  
    77 * License: GPLv2 or later
    88 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
     9 *
     10 * @package Current_Year_Shortcode
    911 */
    1012
    1113if ( ! defined( 'ABSPATH' ) ) {
    12         exit; // Exit if accessed directly
     14        exit; // Exit if accessed directly.
    1315}
    1416
    1517/**
    16  * Retrieve registered trademark symbol (®)
     18 * Retrieve registered trademark symbol.
    1719 *
    18  * @param array $atts Shortcode attributes (not used)
    19  * @return string The registered trademark symbol, escaped for safe output
     20 * @return string The registered trademark symbol, escaped for safe output.
    2021 */
    21 add_shortcode( 't', 'cys_registered_trademark' );
    22 function cys_registered_trademark( $atts ) {
     22function cys_registered_trademark() {
    2323        return '<abbr aria-label="' . esc_attr( 'Registered trademark' ) . '">®</abbr>';
    2424}
     25add_shortcode( 't', 'cys_registered_trademark' );
    2526
    2627/**
    27  * Retrieve trademark symbol (™)
     28 * Retrieve trademark symbol.
    2829 *
    29  * @param array $atts Shortcode attributes (not used)
    30  * @return string The trademark symbol, escaped for safe output
     30 * @return string The trademark symbol, escaped for safe output.
    3131 */
    32 add_shortcode( 'tm', 'cys_trademark' );
    33 function cys_trademark( $atts ) {
     32function cys_trademark() {
    3433        return '<abbr aria-label="' . esc_attr( 'Trademark' ) . '">™</abbr>';
    3534}
     35add_shortcode( 'tm', 'cys_trademark' );
    3636
    3737/**
    38  * Retrieve service mark symbol (℠)
     38 * Retrieve service mark symbol.
    3939 *
    40  * @param array $atts Shortcode attributes (not used)
    41  * @return string The service mark symbol, escaped for safe output
     40 * @return string The service mark symbol, escaped for safe output.
    4241 */
    43 add_shortcode( 'sm', 'cys_servicemark_trademark' );
    44 function cys_servicemark_trademark( $atts ) {
     42function cys_servicemark_trademark() {
    4543        return '<abbr aria-label="' . esc_attr( 'Service mark' ) . '">℠</abbr>';
    4644}
     45add_shortcode( 'sm', 'cys_servicemark_trademark' );
  • current-year-shortcode/trunk/year-kgm.php

    r3504475 r3646096  
    22/**
    33 * Plugin Name: Current Year, Symbols and IP Shortcode
    4  * Version: 2.5
     4 * Version: 2.6
    55 * Description: Get current year, symbols and IP with shortcode.
    66 * Author: KGM Servizi
    … …  
    88 * License: GPLv2 or later
    99 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
     10 *
     11 * @package Current_Year_Shortcode
    1012 */
    1113
    1214if ( ! defined( 'ABSPATH' ) ) {
    13         exit; // Exit if accessed directly
     15        exit; // Exit if accessed directly.
    1416}
    1517
Note: See TracChangeset for help on using the changeset viewer.