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

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

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