Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/code-snippets/php/class-snippet.php

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
1 -
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 +