PluginProbe
Code Snippets / 3.6.3
Code Snippets v3.6.3
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.6.3, at php/class-snippet.php

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