PluginProbe
Current Year, Symbols and IP Shortcode / trunk
Current Year, Symbols and IP Shortcode vtrunk
trunk 2.5 2.6
current-year-shortcode / shortcodes / dates.php

dates.php in Current Year, Symbols and IP Shortcode trunk, at shortcodes/dates.php

317 lines 10.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Includes shortcodes
4 * Plugin: Current Year and Symbols Shortcode
5 * Since: 2.3
6 * Author: KGM Servizi
7 * License: GPLv2 or later
8 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
9 *
10 * @package Current_Year_Shortcode
11 */
12
13 if ( ! defined( 'ABSPATH' ) ) {
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.
24 */
25 function cys_validate_offset( $offset, $type = 'years' ) {
26 // Remove any whitespace.
27 $offset = trim( $offset );
28
29 // Check if offset is empty or 'none'.
30 if ( '' === $offset || 'none' === $offset ) {
31 return 'none';
32 }
33
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
44 if ( 'generic' === $type ) {
45 // For generic strtotime, allow only safe patterns.
46 // Pattern 1: today/yesterday/tomorrow.
47 if ( preg_match( '/^(today|yesterday|tomorrow)$/i', $offset ) ) {
48 return $offset;
49 }
50
51 // Pattern 2: numeric offset with optional time unit.
52 if ( preg_match( '/^([+-]?\s*\d+)(\s*(years?|months?|days?|weeks?|hours?|minutes?|seconds?))?$/i', $offset, $matches ) ) {
53 $num_offset = intval( trim( $matches[1] ) );
54 if ( $num_offset >= -1000 && $num_offset <= 1000 ) {
55 return $offset;
56 }
57 }
58
59 return false;
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;
68 }
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 */
81 function cys_validate_date_format( $format, $type = 'year' ) {
82 $format = trim( $format );
83
84 if ( '' === $format ) {
85 return false;
86 }
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 ) {
95 return false;
96 }
97
98 // Define allowed format characters based on type.
99 $allowed_chars = array(
100 'year' => '/^[yY]+$/',
101 'month' => '/^[FmMn]+$/',
102 'day' => '/^[dDjNwzSt]+$/',
103 'date' => '/^[dDjlNSwzWFmMntLoYyaABgGhHisueIOPTZcrU\s\-\/\.\,\:\;]+$/',
104 );
105
106 // Check if format matches allowed pattern for the type.
107 if ( isset( $allowed_chars[ $type ] ) && preg_match( $allowed_chars[ $type ], $format ) ) {
108 return $format;
109 }
110
111 return false;
112 }
113
114 /**
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 */
125 function 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 */
135 function 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 */
153 function 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 */
195 function 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 */
227 function cys_render_date_shortcode( $atts, $tag ) {
228 $config = cys_date_shortcode_config();
229 $spec = $config[ $tag ];
230
231 $atts = shortcode_atts(
232 array(
233 'format' => 'error',
234 'offset' => 'none',
235 ),
236 $atts,
237 $tag
238 );
239
240 $atts = cys_sanitize_shortcode_atts( $atts );
241
242 $validated_offset = cys_validate_offset( $atts['offset'], $spec['offset_type'] );
243 if ( false === $validated_offset ) {
244 return cys_error( 'Invalid offset value!' );
245 }
246
247 if ( 'error' !== $atts['format'] ) {
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!' );
251 }
252 } else {
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 */
274 function cys_year( $atts ) {
275 return cys_render_date_shortcode( $atts, 'y' );
276 }
277 add_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 */
287 function cys_month( $atts ) {
288 return cys_render_date_shortcode( $atts, 'm' );
289 }
290 add_shortcode( 'm', 'cys_month' );
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 */
300 function cys_day( $atts ) {
301 return cys_render_date_shortcode( $atts, 'd' );
302 }
303 add_shortcode( 'd', 'cys_day' );
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 */
313 function cys_current_date( $atts ) {
314 return cys_render_date_shortcode( $atts, 'dmy' );
315 }
316 add_shortcode( 'dmy', 'cys_current_date' );
317