PluginProbe
Code Snippets / trunk
Code Snippets vtrunk
4.0.0-beta.2 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 All 65 releases
code-snippets / php / Model / Snippet.php

Snippet.php in Code Snippets trunk, at php/Model/Snippet.php

592 lines 17.4 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\Model;
4
5 use DateTime;
6 use DateTimeZone;
7 use Exception;
8 use function Code_Snippets\code_snippets_build_tags_array;
9 use function Code_Snippets\Utils\get_self_option;
10
11 /**
12 * A snippet object.
13 *
14 * @since 2.4.0
15 * @package Code_Snippets
16 *
17 * @property int $id The database ID.
18 * @property string $name The snippet title.
19 * @property string $desc The formatted description.
20 * @property string $code The executable code.
21 * @property array<string> $tags An array of the tags.
22 * @property string $scope The scope name.
23 * @property int $condition_id ID of the condition this snippet is linked to.
24 * @property int $priority Execution priority.
25 * @property bool $active The active status.
26 * @property bool $trashed Whether the snippet is marked as 'deleted'.
27 * @property bool $locked Whether the snippet is locked from modification or deletion.
28 * @property bool $network true if is multisite-wide snippet, false if site-wide.
29 * @property bool $shared_network Whether the snippet is a shared network snippet.
30 * @property string $modified The date and time when the snippet data was most recently saved to the database.
31 * @property array{string,int}|null $code_error Code error encountered when last testing snippet code.
32 * @property string|null $code_error_trace Stack trace captured when last testing snippet code.
33 * @property int $revision Revision or version number of snippet.
34 * @property int $cloud_id The cloud ID of the snippet, if applicable.
35 * @property bool $is_cloud_owner Whether the user is the owner of the cloud snippet, if applicable.
36 * @property int $created_by User ID of the snippet's original author. 0 if unknown.
37 * @property int $updated_by User ID of the last person to save the snippet. 0 if unknown.
38 *
39 * @property-read int $last_active Timestamp of when the snippet was last active, if available.
40 * @property-read string $display_name The snippet name if it exists or a placeholder if it does not.
41 * @property-read string $tags_list The tags in string list format.
42 * @property-read string $scope_icon The dashicon used to represent the current scope.
43 * @property-read string $scope_name Human-readable description of the snippet type.
44 * @property-read string $type The type of snippet.
45 * @property-read string $lang The language that the snippet code is written in.
46 * @property-read int $modified_timestamp The last modification date in Unix timestamp format.
47 * @property-read DateTime $modified_local The last modification date in the local timezone.
48 * @property-read DateTime $modified_iso The last modification date in ISO 8601 format.
49 * @property-read bool $is_pro Whether the snippet type is pro-only.
50 * @property-read string $cloud_id_owner Cloud ownership information as a single value, if applicable.
51 */
52 class Snippet extends Model {
53
54 /**
55 * MySQL datetime format (YYYY-MM-DD hh:mm:ss).
56 */
57 public const DATE_FORMAT = 'Y-m-d H:i:s';
58
59 /**
60 * Default value used for a datetime variable.
61 */
62 public const DEFAULT_DATE = '0000-00-00 00:00:00';
63
64 /**
65 * Default values for snippet fields.
66 *
67 * @var array<string, mixed>
68 */
69 protected static array $default_values = [
70 'id' => 0,
71 'name' => '',
72 'desc' => '',
73 'code' => '',
74 'tags' => [],
75 'scope' => 'global',
76 'condition_id' => 0,
77 'active' => false,
78 'trashed' => false,
79 'locked' => false,
80 'priority' => 10,
81 'network' => null,
82 'shared_network' => null,
83 'modified' => null,
84 'code_error' => null,
85 'code_error_trace' => null,
86 'revision' => 1,
87 'cloud_id' => 0,
88 'is_cloud_owner' => false,
89 'created_by' => 0,
90 'updated_by' => 0,
91 ];
92
93 /**
94 * Field name aliases.
95 *
96 * @var array<string, string>
97 */
98 protected static array $field_aliases = [
99 'description' => 'desc',
100 'language' => 'lang',
101 'conditionId' => 'condition_id',
102 ];
103
104 /**
105 * Prepare a value before it is stored.
106 *
107 * @param mixed $value Value to prepare.
108 * @param string $field Field name.
109 *
110 * @return mixed Value in the correct format.
111 */
112 protected function prepare_field( $value, string $field ) {
113 switch ( $field ) {
114 case 'id':
115 case 'priority':
116 case 'condition_id':
117 case 'cloud_id':
118 case 'revision':
119 case 'created_by':
120 case 'updated_by':
121 return absint( $value );
122
123 case 'tags':
124 return code_snippets_build_tags_array( $value );
125
126 case 'active':
127 if ( -1 === intval( $value ) ) {
128 $this->trashed = true;
129 return false;
130 }
131
132 return 1 === intval( $value ) && ! $this->trashed && ! $this->is_condition();
133
134 case 'locked':
135 case 'is_cloud_owner':
136 return is_bool( $value ) ? $value : (bool) $value;
137
138 default:
139 return $value;
140 }
141 }
142
143 /**
144 * Prepare the scope by ensuring that it is a valid choice
145 *
146 * @param int|string $scope The field as provided.
147 *
148 * @return string The field in the correct format.
149 * @noinspection PhpUnused
150 */
151 protected function prepare_scope( $scope ) {
152 $scopes = self::get_all_scopes();
153
154 if ( in_array( $scope, $scopes, true ) ) {
155 return $scope;
156 }
157
158 if ( is_numeric( $scope ) && isset( $scopes[ $scope ] ) ) {
159 return $scopes[ $scope ];
160 }
161
162 return $this->fields['scope'];
163 }
164
165 /**
166 * If $network is anything other than true, set it to false
167 *
168 * @param bool $network The field as provided.
169 *
170 * @return bool The field in the correct format.
171 * @noinspection PhpUnused
172 */
173 protected function prepare_network( bool $network ): bool {
174 if ( null === $network && function_exists( 'is_network_admin' ) ) {
175 return is_network_admin();
176 }
177
178 return true === $network;
179 }
180
181 /**
182 * Set the 'cloud_id', and possibly the 'is_cloud_owner' fields.
183 *
184 * @param string|int $cloud_id Cloud identifier, possibly with cloud ownership information attached.
185 *
186 * @return int
187 *
188 * @noinspection PhpUnused
189 */
190 protected function prepare_cloud_id( $cloud_id ): int {
191 if ( is_numeric( $cloud_id ) ) {
192 return absint( $cloud_id );
193 }
194
195 if ( is_null( $cloud_id ) ) {
196 return 0;
197 }
198
199 $parts = explode( '_', $cloud_id );
200
201 $is_cloud_owner = isset( $parts[1] )
202 ? filter_var( $parts[1], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE )
203 : null;
204
205 if ( ! is_null( $is_cloud_owner ) ) {
206 $this->is_cloud_owner = $is_cloud_owner;
207 }
208
209 return isset( $parts[0] ) && is_numeric( $parts[0] )
210 ? absint( $parts[0] )
211 : $this->cloud_id;
212 }
213
214 /**
215 * Add a new tag
216 *
217 * @param string $tag Tag content to add to list.
218 */
219 public function add_tag( string $tag ) {
220 $this->fields['tags'][] = $tag;
221 }
222
223 /**
224 * Determine if the snippet is a condition.
225 *
226 * @return bool
227 */
228 public function is_condition(): bool {
229 return 'condition' === $this->scope;
230 }
231
232 /**
233 * Determine the type of code a given scope will produce.
234 *
235 * @param string $scope Scope name.
236 *
237 * @return string The snippet type – will be a filename extension.
238 */
239 public static function get_type_from_scope( string $scope ): string {
240 if ( '-css' === substr( $scope, -4 ) ) {
241 return 'css';
242 } elseif ( '-js' === substr( $scope, -3 ) ) {
243 return 'js';
244 } elseif ( 'content' === substr( $scope, -7 ) ) {
245 return 'html';
246 } elseif ( 'condition' === $scope ) {
247 return 'cond';
248 } else {
249 return 'php';
250 }
251 }
252
253 /**
254 * Determine the type of code this snippet is, based on its scope
255 *
256 * @return string The snippet type – will be a filename extension.
257 */
258 public function get_type(): string {
259 return self::get_type_from_scope( $this->scope );
260 }
261
262 /**
263 * Retrieve a list of all valid types.
264 *
265 * @return string[]
266 */
267 public static function get_types(): array {
268 return [ 'php', 'html', 'css', 'js', 'cond' ];
269 }
270
271 /**
272 * Retrieve the timestamp of when the snippet was last active, if available.
273 *
274 * @return string
275 * @noinspection PhpUnused
276 */
277 protected function get_last_active(): string {
278 $recently_active = get_self_option( $this->network, 'recently_active_snippets', [] );
279 return $recently_active[ (string) $this->id ] ?? 0;
280 }
281
282 /**
283 * Determine the language that the snippet code is written in, based on the scope
284 *
285 * @return string The name of a language filename extension.
286 * @noinspection PhpUnused
287 */
288 protected function get_lang(): string {
289 return 'cond' === $this->type ? 'json' : $this->type;
290 }
291
292 /**
293 * Prepare the modification field by ensuring it is in the correct format.
294 *
295 * @param DateTime|string $modified Snippet modification date.
296 *
297 * @return string
298 * @noinspection PhpUnused
299 */
300 protected function prepare_modified( $modified ): ?string {
301
302 // If the supplied value is a DateTime object, convert it to string representation.
303 if ( $modified instanceof DateTime ) {
304 return $modified->format( self::DATE_FORMAT );
305 }
306
307 // If the supplied value is probably a timestamp, attempt to convert it to a string.
308 if ( is_numeric( $modified ) ) {
309 return gmdate( self::DATE_FORMAT, $modified );
310 }
311
312 // If the supplied value is a string, check it is not just the default value.
313 if ( is_string( $modified ) && self::DEFAULT_DATE !== $modified ) {
314 return $modified;
315 }
316
317 // Otherwise, discard the supplied value.
318 return null;
319 }
320
321 /**
322 * Update the last modification date to the current date and time.
323 *
324 * @return void
325 */
326 public function update_modified() {
327 $this->modified = gmdate( self::DATE_FORMAT );
328 }
329
330 /**
331 * Retrieve the snippet title if set or a placeholder title if not.
332 *
333 * @return string
334 * @noinspection PhpUnused
335 */
336 protected function get_display_name(): string {
337 // translators: %s: snippet identifier.
338 return empty( $this->name ) ? sprintf( esc_html__( 'Snippet #%d', 'code-snippets' ), $this->id ) : $this->name;
339 }
340
341 /**
342 * Retrieve the tags in list format
343 *
344 * @return string The tags separated by a comma and a space.
345 * @noinspection PhpUnused
346 */
347 protected function get_tags_list(): string {
348 return implode( ', ', $this->tags );
349 }
350
351 /**
352 * Retrieve a list of all available scopes
353 *
354 * @return array<string> List of scope names.
355 *
356 * phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
357 */
358 public static function get_all_scopes(): array {
359 return array(
360 'global', 'admin', 'front-end', 'single-use',
361 'content', 'head-content', 'body-content', 'footer-content',
362 'admin-css', 'site-css',
363 'site-head-js', 'site-footer-js',
364 'condition',
365 );
366 }
367
368 /**
369 * Retrieve a list of all scope icons
370 *
371 * @return array<string, string> Scope name keyed to the class name of a dashicon.
372 */
373 public static function get_scope_icons(): array {
374 return array(
375 'global' => 'admin-site',
376 'admin' => 'admin-tools',
377 'front-end' => 'admin-appearance',
378 'single-use' => 'clock',
379 'content' => 'shortcode',
380 'head-content' => 'editor-code',
381 'body-content' => 'editor-code',
382 'footer-content' => 'editor-code',
383 'admin-css' => 'dashboard',
384 'site-css' => 'admin-customizer',
385 'site-head-js' => 'media-code',
386 'site-footer-js' => 'media-code',
387 'condition' => 'randomize',
388 );
389 }
390
391 /**
392 * Retrieve the string representation of the scope
393 *
394 * @return string The name of the scope.
395 * @noinspection PhpUnused
396 */
397 protected function get_scope_name(): string {
398 switch ( $this->scope ) {
399 case 'global':
400 return __( 'Global function', 'code-snippets' );
401 case 'admin':
402 return __( 'Admin function', 'code-snippets' );
403 case 'front-end':
404 return __( 'Front-end function', 'code-snippets' );
405 case 'single-use':
406 return __( 'Single-use function', 'code-snippets' );
407 case 'content':
408 return __( 'Content', 'code-snippets' );
409 case 'head-content':
410 return __( 'Head content', 'code-snippets' );
411 case 'body-content':
412 return __( 'Body content', 'code-snippets' );
413 case 'footer-content':
414 return __( 'Footer content', 'code-snippets' );
415 case 'admin-css':
416 return __( 'Admin styles', 'code-snippets' );
417 case 'site-css':
418 return __( 'Front-end styles', 'code-snippets' );
419 case 'site-head-js':
420 return __( 'Head scripts', 'code-snippets' );
421 case 'site-footer-js':
422 return __( 'Footer scripts', 'code-snippets' );
423 }
424
425 return '';
426 }
427
428 /**
429 * Retrieve the icon used for the current scope
430 *
431 * @return string A dashicon name.
432 * @noinspection PhpUnused
433 */
434 protected function get_scope_icon(): string {
435 $icons = self::get_scope_icons();
436
437 return $icons[ $this->scope ];
438 }
439
440 /**
441 * Determine if the snippet is a shared network snippet
442 *
443 * @return bool Whether the snippet is a shared network snippet.
444 * @noinspection PhpUnused
445 */
446 protected function get_shared_network(): bool {
447 if ( isset( $this->fields['shared_network'] ) ) {
448 return $this->fields['shared_network'];
449 }
450
451 if ( ! is_multisite() || ! $this->fields['network'] ) {
452 $this->fields['shared_network'] = false;
453 } else {
454 $shared_network_snippets = get_site_option( 'shared_network_snippets', array() );
455 $this->fields['shared_network'] = in_array( $this->fields['id'], $shared_network_snippets, true );
456 }
457
458 return $this->fields['shared_network'];
459 }
460
461 /**
462 * Retrieve the snippet modification date as a timestamp.
463 *
464 * @return int Timestamp value.
465 * @noinspection PhpUnused
466 */
467 protected function get_modified_timestamp(): int {
468 $datetime = DateTime::createFromFormat( self::DATE_FORMAT, $this->modified, new DateTimeZone( 'UTC' ) );
469
470 return $datetime ? $datetime->getTimestamp() : 0;
471 }
472
473 /**
474 * Retrieve the modification date as an ISO 8601 string, including the UTC
475 * offset.
476 *
477 * `$modified` is stored as 'Y-m-d H:i:s' in UTC, which carries no offset, so
478 * anything parsing it — a browser, in particular — is free to read it as
479 * local time and land the snippet hours away from when it was really saved.
480 * This is the form to hand to clients.
481 *
482 * @return string|null ISO 8601 date, or null if no modification date is set.
483 * @noinspection PhpUnused
484 */
485 protected function get_modified_iso(): ?string {
486 $timestamp = $this->get_modified_timestamp();
487
488 return $timestamp ? gmdate( 'c', $timestamp ) : null;
489 }
490
491 /**
492 * Retrieve the modification time in the local timezone.
493 *
494 * @return DateTime
495 * @noinspection PhpUnused
496 */
497 protected function get_modified_local(): DateTime {
498 $datetime = DateTime::createFromFormat( self::DATE_FORMAT, $this->modified, new DateTimeZone( 'UTC' ) );
499
500 if ( function_exists( 'wp_timezone' ) ) {
501 $timezone = wp_timezone();
502 } else {
503 $timezone = get_option( 'timezone_string' );
504
505 // Calculate the timezone manually if it is not available.
506 if ( ! $timezone ) {
507 $offset = (float) get_option( 'gmt_offset' );
508 $hours = (int) $offset;
509 $minutes = ( $offset - $hours ) * 60;
510
511 $sign = ( $offset < 0 ) ? '-' : '+';
512 $timezone = sprintf( '%s%02d:%02d', $sign, abs( $hours ), abs( $minutes ) );
513 }
514
515 try {
516 $timezone = new DateTimeZone( $timezone );
517 } catch ( Exception $exception ) {
518 return $datetime;
519 }
520 }
521
522 $datetime->setTimezone( $timezone );
523 return $datetime;
524 }
525
526 /**
527 * Retrieve the last modified time, nicely formatted for readability.
528 *
529 * @param bool $include_html Whether to include HTML in the output.
530 *
531 * @return string
532 */
533 public function format_modified( bool $include_html = true ): string {
534 if ( ! $this->modified ) {
535 return '';
536 }
537
538 $timestamp = $this->modified_timestamp;
539 $time_diff = time() - $timestamp;
540 $local_time = $this->modified_local;
541
542 if ( $time_diff >= 0 && $time_diff < YEAR_IN_SECONDS ) {
543 // translators: %s: Human-readable time difference.
544 $human_time = sprintf( __( '%s ago', 'code-snippets' ), human_time_diff( $timestamp ) );
545 } else {
546 $human_time = $local_time->format( __( 'Y/m/d', 'code-snippets' ) );
547 }
548
549 if ( ! $include_html ) {
550 return $human_time;
551 }
552
553 // translators: 1: date format, 2: time format.
554 $date_format = _x( '%1$s at %2$s', 'date and time format', 'code-snippets' );
555 $date_format = sprintf( $date_format, get_option( 'date_format' ), get_option( 'time_format' ) );
556
557 return sprintf( '<span title="%s">%s</span>', $local_time->format( $date_format ), $human_time );
558 }
559
560 /**
561 * Determine whether the current snippet type is pro-only.
562 *
563 * @return bool
564 *
565 * @noinspection PhpUnused
566 */
567 protected function get_is_pro(): bool {
568 return 'css' === $this->type || 'js' === $this->type || 'cond' === $this->type;
569 }
570
571 /**
572 * Concatonate the 'cloud_id' and 'is_cloud_owner' fields to provide a single value representing the ownership
573 * status of the cloud snippet, if applicable.
574 *
575 * @return string
576 *
577 * @noinspection PhpUnused
578 */
579 protected function get_cloud_id_owner(): string {
580 return $this->cloud_id
581 ? sprintf( '%d_%d', $this->cloud_id, $this->is_cloud_owner ? '1' : '0' )
582 : '';
583 }
584
585 /**
586 * Increment the revision number by one.
587 */
588 public function increment_revision() {
589 ++$this->revision;
590 }
591 }
592