PluginProbe
Code Snippets / 4.0.0-beta.1
Code Snippets v4.0.0-beta.1
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 4.0.0-beta.1, at php/Model/Snippet.php

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