PluginProbe
Code Snippets / 3.2.1
Code Snippets v3.2.1
3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 3.0.1 All 64 releases
code-snippets / build / php / class-snippet.php

class-snippet.php in Code Snippets 3.2.1, at build/php/class-snippet.php

631 lines 16.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Code_Snippets;
4
5 use DateTime;
6 use DateTimeZone;
7
8 /**
9 * A snippet object.
10 *
11 * @since 2.4.0
12 * @package Code_Snippets
13 *
14 * @property int $id The database ID.
15 * @property string $name The snippet title.
16 * @property string $desc The formatted description.
17 * @property string $code The executable code.
18 * @property array $tags An array of the tags.
19 * @property string $scope The scope name.
20 * @property int $priority Execution priority.
21 * @property bool $active The active status.
22 * @property bool $network true if is multisite-wide snippet, false if site-wide.
23 * @property bool $shared_network Whether the snippet is a shared network snippet.
24 * @property string $modified The date and time when the snippet data was most recently saved to the database.
25 *
26 * @property-read string $display_name The snippet name if it exists or a placeholder if it does not.
27 * @property-read string $tags_list The tags in string list format.
28 * @property-read string $scope_icon The dashicon used to represent the current scope.
29 * @property-read string $scope_name Human-readable description of the snippet type.
30 * @property-read string $type The type of snippet.
31 * @property-read string $lang The language that the snippet code is written in.
32 * @property-read int $modified_timestamp The last modification date in Unix timestamp format.
33 * @property-read DateTime $modified_local The last modification date in the local timezone.
34 * @property-read string $type_desc Human-readable description of the snippet type.
35 */
36 class Snippet {
37
38 /**
39 * MySQL datetime format (YYYY-MM-DD hh:mm:ss).
40 */
41 const DATE_FORMAT = 'Y-m-d H:i:s';
42
43 /**
44 * Default value used for a datetime variable.
45 */
46 const DEFAULT_DATE = '0000-00-00 00:00:00';
47
48 /**
49 * The snippet metadata fields.
50 * Initialized with default values.
51 *
52 * @var array Two-dimensional array of field names keyed to current values.
53 */
54 private $fields = array(
55 'id' => 0,
56 'name' => '',
57 'desc' => '',
58 'code' => '',
59 'tags' => array(),
60 'scope' => 'global',
61 'active' => false,
62 'priority' => 10,
63 'network' => null,
64 'shared_network' => null,
65 'modified' => null,
66 );
67
68 /**
69 * List of field aliases
70 *
71 * @var array Two-dimensional array of field alias names keyed to actual field names.
72 */
73 private static $field_aliases = array(
74 'description' => 'desc',
75 'language' => 'lang',
76 );
77
78 /**
79 * Constructor function
80 *
81 * @param array|object $fields Initial snippet fields.
82 */
83 public function __construct( $fields = null ) {
84
85 // If we've accidentally passed a snippet object, then fetch its fields before constructing the new object.
86 if ( is_object( $fields ) && method_exists( $fields, 'get_fields' ) ) {
87 $fields = $fields->get_fields();
88 }
89
90 $this->set_fields( $fields );
91 }
92
93 /**
94 * Set all snippet fields from an array or object.
95 * Invalid fields will be ignored.
96 *
97 * @param array|object $fields List of fields.
98 */
99 public function set_fields( $fields ) {
100
101 /* Only accept arrays or objects */
102 if ( ! $fields || is_string( $fields ) ) {
103 return;
104 }
105
106 /* Convert objects into arrays */
107 if ( is_object( $fields ) ) {
108 $fields = get_object_vars( $fields );
109 }
110
111 /* Loop through the passed fields and set them */
112 foreach ( $fields as $field => $value ) {
113 $this->set_field( $field, $value );
114 }
115 }
116
117 /**
118 * Retrieve all snippet fields
119 *
120 * @return array Two-dimensional array of field names keyed to current values
121 */
122 public function get_fields() {
123 return $this->fields;
124 }
125
126 /**
127 * Internal function for validating the name of a field
128 *
129 * @param string $field A field name.
130 *
131 * @return string The validated field name.
132 */
133 private function validate_field_name( $field ) {
134
135 /* If a field alias is set, remap it to the valid field name */
136 if ( isset( self::$field_aliases[ $field ] ) ) {
137 return self::$field_aliases[ $field ];
138 }
139
140 return $field;
141 }
142
143 /**
144 * Check if a field is set
145 *
146 * @param string $field The field name.
147 *
148 * @return bool Whether the field is set.
149 */
150 public function __isset( $field ) {
151 $field = $this->validate_field_name( $field );
152
153 return isset( $this->fields[ $field ] ) || method_exists( $this, 'get_' . $field );
154 }
155
156 /**
157 * Retrieve a field's value
158 *
159 * @param string $field The field name.
160 *
161 * @return mixed The field value
162 */
163 public function __get( $field ) {
164 $field = $this->validate_field_name( $field );
165
166 if ( method_exists( $this, 'get_' . $field ) ) {
167 return call_user_func( array( $this, 'get_' . $field ) );
168 }
169
170 if ( ! $this->is_allowed_field( $field ) ) {
171 if ( WP_DEBUG ) {
172 // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
173 trigger_error( 'Trying to access invalid property on Snippets class: ' . esc_html( $field ), E_WARNING );
174 }
175
176 return null;
177 }
178
179 return $this->fields[ $field ];
180 }
181
182 /**
183 * Set the value of a field
184 *
185 * @param string $field The field name.
186 * @param mixed $value The field value.
187 */
188 public function __set( $field, $value ) {
189 $field = $this->validate_field_name( $field );
190
191 if ( ! $this->is_allowed_field( $field ) ) {
192 if ( WP_DEBUG ) {
193 // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
194 trigger_error( 'Trying to set invalid property on Snippets class: ' . esc_html( $field ), E_WARNING );
195 }
196
197 return;
198 }
199
200 /* Check if the field value should be filtered */
201 if ( method_exists( $this, 'prepare_' . $field ) ) {
202 $value = call_user_func( array( $this, 'prepare_' . $field ), $value );
203 }
204
205 $this->fields[ $field ] = $value;
206 }
207
208 /**
209 * Retrieve the list of fields allowed to be written to
210 *
211 * @return array Single-dimensional array of field names.
212 */
213 public function get_allowed_fields() {
214 return array_keys( $this->fields ) + array_keys( self::$field_aliases );
215 }
216
217 /**
218 * Determine whether a field is allowed to be written to
219 *
220 * @param string $field The field name.
221 *
222 * @return bool true if the is allowed, false if invalid.
223 */
224 public function is_allowed_field( $field ) {
225 return array_key_exists( $field, $this->fields ) || array_key_exists( $field, self::$field_aliases );
226 }
227
228 /**
229 * Safely set the value for a field
230 * If the field name is invalid, false will be returned instead of an error thrown.
231 *
232 * @param string $field The field name.
233 * @param mixed $value The field value.
234 *
235 * @return bool true if the field was set successfully, false if the field name is invalid.
236 */
237 public function set_field( $field, $value ) {
238 if ( ! $this->is_allowed_field( $field ) ) {
239 return false;
240 }
241
242 $this->__set( $field, $value );
243
244 return true;
245 }
246
247 /**
248 * Add a new tag
249 *
250 * @param string $tag Tag content to add to list.
251 */
252 public function add_tag( $tag ) {
253 $this->fields['tags'][] = $tag;
254 }
255
256 /**
257 * Prepare the ID by ensuring it is an absolute integer
258 *
259 * @param int $id The field as provided.
260 *
261 * @return int The field in the correct format.
262 */
263 private function prepare_id( $id ) {
264 return absint( $id );
265 }
266
267 /**
268 * Prepare the scope by ensuring that it is a valid choice
269 *
270 * @param int|string $scope The field as provided.
271 *
272 * @return string The field in the correct format.
273 */
274 private function prepare_scope( $scope ) {
275 $scopes = self::get_all_scopes();
276
277 if ( in_array( $scope, $scopes, true ) ) {
278 return $scope;
279 }
280
281 if ( is_numeric( $scope ) && isset( $scopes[ $scope ] ) ) {
282 return $scopes[ $scope ];
283 }
284
285 return $this->fields['scope'];
286 }
287
288 /**
289 * Prepare the snippet tags by ensuring they are in the correct format
290 *
291 * @param string|array $tags The field as provided.
292 *
293 * @return array The field in the correct format.
294 */
295 private function prepare_tags( $tags ) {
296 return code_snippets_build_tags_array( $tags );
297 }
298
299 /**
300 * Prepare the active field by ensuring it is the correct type
301 *
302 * @param bool|int $active The field as provided.
303 *
304 * @return bool The field in the correct format.
305 */
306 private function prepare_active( $active ) {
307
308 if ( is_bool( $active ) ) {
309 return $active;
310 }
311
312 return (bool) $active;
313 }
314
315 /**
316 * Prepare the priority field by ensuring it is an integer
317 *
318 * @param int $priority The field as provided.
319 *
320 * @return int The field in the correct format.
321 */
322 private function prepare_priority( $priority ) {
323 return intval( $priority );
324 }
325
326 /**
327 * If $network is anything other than true, set it to false
328 *
329 * @param bool $network The field as provided.
330 *
331 * @return bool The field in the correct format.
332 */
333 private function prepare_network( $network ) {
334
335 if ( null === $network && function_exists( 'is_network_admin' ) ) {
336 return is_network_admin();
337 }
338
339 return true === $network;
340 }
341
342 /**
343 * Determine the type of code this snippet is, based on its scope
344 *
345 * @return string The snippet type – will be a filename extension.
346 */
347 private function get_type() {
348 if ( '-css' === substr( $this->scope, -4 ) ) {
349 return 'css';
350 }
351
352 if ( '-js' === substr( $this->scope, -3 ) ) {
353 return 'js';
354 }
355
356 if ( 'content' === substr( $this->scope, -7 ) ) {
357 return 'html';
358 }
359
360 return 'php';
361 }
362
363 /**
364 * Retrieve a list of all valid types.
365 *
366 * @return string[]
367 */
368 public static function get_types() {
369 return [ 'php', 'html', 'css', 'js' ];
370 }
371
372 /**
373 * Retrieve description of snippet type.
374 *
375 * @return string
376 */
377 private function get_type_desc() {
378 $labels = [
379 'php' => __( 'Functions', 'code-snippets' ),
380 'html' => __( 'Content', 'code-snippets' ),
381 'css' => __( 'Styles', 'code-snippets' ),
382 'js' => __( 'Scripts', 'code-snippets' ),
383 ];
384
385 return isset( $labels[ $this->type ] ) ? $labels[ $this->type ] : strtoupper( $this->type );
386 }
387
388 /**
389 * Determine the language that the snippet code is written in, based on the scope
390 *
391 * @return string The name of a language filename extension.
392 */
393 private function get_lang() {
394 return $this->type;
395 }
396
397 /**
398 * Prepare the modification field by ensuring it is in the correct format.
399 *
400 * @param DateTime|string $modified Snippet modification date.
401 *
402 * @return string
403 */
404 private function prepare_modified( $modified ) {
405
406 /* if the supplied value is a DateTime object, convert it to string representation */
407 if ( $modified instanceof DateTime ) {
408 return $modified->format( self::DATE_FORMAT );
409 }
410
411 /* if the supplied value is probably a timestamp, attempt to convert it to a string */
412 if ( is_numeric( $modified ) ) {
413 return gmdate( self::DATE_FORMAT, $modified );
414 }
415
416 /* if the supplied value is a string, check it is not just the default value */
417 if ( is_string( $modified ) && self::DEFAULT_DATE !== $modified ) {
418 return $modified;
419 }
420
421 /* otherwise, discard the supplied value */
422
423 return null;
424 }
425
426 /**
427 * Update the last modification date to the current date and time.
428 */
429 public function update_modified() {
430 $this->modified = gmdate( self::DATE_FORMAT );
431 }
432
433 /**
434 * Retrieve the snippet title if set or a placeholder title if not.
435 *
436 * @return string
437 */
438 private function get_display_name() {
439 /* translators: %d: snippet ID */
440 return empty( $this->name ) ? sprintf( esc_html__( 'Untitled #%d', 'code-snippets' ), $this->id ) : $this->name;
441 }
442
443 /**
444 * Retrieve the tags in list format
445 *
446 * @return string The tags separated by a comma and a space.
447 */
448 private function get_tags_list() {
449 return implode( ', ', $this->tags );
450 }
451
452 /**
453 * Retrieve a list of all available scopes
454 *
455 * @return array Single-dimensional array of scope names.
456 *
457 * @phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
458 */
459 public static function get_all_scopes() {
460 return array(
461 'global', 'admin', 'front-end', 'single-use',
462 'content', 'head-content', 'footer-content',
463 'admin-css', 'site-css',
464 'site-head-js', 'site-footer-js',
465 );
466 }
467
468 /**
469 * Retrieve a list of all scope icons
470 *
471 * @return array Two-dimensional array with scope name keyed to the class name of a dashicon.
472 */
473 public static function get_scope_icons() {
474 return array(
475 'global' => 'admin-site',
476 'admin' => 'admin-tools',
477 'front-end' => 'admin-appearance',
478 'single-use' => 'clock',
479 'content' => 'shortcode',
480 'head-content' => 'editor-code',
481 'footer-content' => 'editor-code',
482 'admin-css' => 'dashboard',
483 'site-css' => 'admin-customizer',
484 'site-head-js' => 'media-code',
485 'site-footer-js' => 'media-code',
486 );
487 }
488
489 /**
490 * Retrieve the string representation of the scope
491 *
492 * @return string The name of the scope.
493 */
494 private function get_scope_name() {
495 switch ( $this->scope ) {
496 case 'global':
497 return __( 'Global function', 'code-snippets' );
498 case 'admin':
499 return __( 'Admin function', 'code-snippets' );
500 case 'front-end':
501 return __( 'Front-end function', 'code-snippets' );
502 case 'single-use':
503 return __( 'Single-use function', 'code-snippets' );
504 case 'content':
505 return __( 'Content', 'code-snippets' );
506 case 'head-content':
507 return __( 'Head content', 'code-snippets' );
508 case 'footer-content':
509 return __( 'Footer content', 'code-snippets' );
510 case 'admin-css':
511 return __( 'Admin styles', 'code-snippets' );
512 case 'site-css':
513 return __( 'Front-end styles', 'code-snippets' );
514 case 'site-head-js':
515 return __( 'Head styles', 'code-snippets' );
516 case 'site-footer-js':
517 return __( 'Footer styles', 'code-snippets' );
518 }
519
520 return '';
521 }
522
523 /**
524 * Retrieve the icon used for the current scope
525 *
526 * @return string A dashicon name.
527 */
528 private function get_scope_icon() {
529 $icons = self::get_scope_icons();
530
531 return $icons[ $this->scope ];
532 }
533
534 /**
535 * Determine if the snippet is a shared network snippet
536 *
537 * @return bool Whether the snippet is a shared network snippet.
538 */
539 private function get_shared_network() {
540
541 if ( isset( $this->fields['shared_network'] ) ) {
542 return $this->fields['shared_network'];
543 }
544
545 if ( ! is_multisite() || ! $this->fields['network'] ) {
546 $this->fields['shared_network'] = false;
547 } else {
548 $shared_network_snippets = get_site_option( 'shared_network_snippets', array() );
549 $this->fields['shared_network'] = in_array( $this->fields['id'], $shared_network_snippets, true );
550 }
551
552 return $this->fields['shared_network'];
553 }
554
555 /**
556 * Retrieve the snippet modification date as a timestamp.
557 *
558 * @return int Timestamp value.
559 */
560 private function get_modified_timestamp() {
561 $datetime = DateTime::createFromFormat( self::DATE_FORMAT, $this->modified, new DateTimeZone( 'UTC' ) );
562
563 return $datetime ? $datetime->getTimestamp() : 0;
564 }
565
566 /**
567 * Retrieve the modification time in the local timezone.
568 *
569 * @return DateTime
570 */
571 private function get_modified_local() {
572
573 if ( function_exists( 'wp_timezone' ) ) {
574 $timezone = wp_timezone();
575 } else {
576 $timezone = get_option( 'timezone_string' );
577
578 /* calculate the timezone manually if it is not available */
579 if ( ! $timezone ) {
580 $offset = (float) get_option( 'gmt_offset' );
581 $hours = (int) $offset;
582 $minutes = ( $offset - $hours ) * 60;
583
584 $sign = ( $offset < 0 ) ? '-' : '+';
585 $timezone = sprintf( '%s%02d:%02d', $sign, abs( $hours ), abs( $minutes ) );
586 }
587
588 $timezone = new DateTimeZone( $timezone );
589 }
590
591 $datetime = DateTime::createFromFormat( self::DATE_FORMAT, $this->modified, new DateTimeZone( 'UTC' ) );
592 $datetime->setTimezone( $timezone );
593
594 return $datetime;
595 }
596
597 /**
598 * Retrieve the last modified time, nicely formatted for readability.
599 *
600 * @param boolean $include_html Whether to include HTML in the output.
601 *
602 * @return string
603 */
604 public function format_modified( $include_html = true ) {
605 if ( ! $this->modified ) {
606 return '';
607 }
608
609 $timestamp = $this->modified_timestamp;
610 $time_diff = time() - $timestamp;
611 $local_time = $this->modified_local;
612
613 if ( $time_diff >= 0 && $time_diff < YEAR_IN_SECONDS ) {
614 /* translators: %s: Human-readable time difference. */
615 $human_time = sprintf( __( '%s ago', 'code-snippets' ), human_time_diff( $timestamp ) );
616 } else {
617 $human_time = $local_time->format( __( 'Y/m/d', 'code-snippets' ) );
618 }
619
620 if ( ! $include_html ) {
621 return $human_time;
622 }
623
624 /* translators: 1: date format, 2: time format */
625 $date_format = _x( '%1$s \a\t %2$s', 'date and time format', 'code-snippets' );
626 $date_format = sprintf( $date_format, get_option( 'date_format' ), get_option( 'time_format' ) );
627
628 return sprintf( '<span title="%s">%s</span>', $local_time->format( $date_format ), $human_time );
629 }
630 }
631