PluginProbe
Code Snippets / 3.10.0
Code Snippets v3.10.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 / Model / Snippet.php

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

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