PluginProbe
Code Snippets / 3.0.0
Code Snippets v3.0.0
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 / php / class-snippet.php

class-snippet.php in Code Snippets 3.0.0, at php/class-snippet.php

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