PluginProbe
Code Snippets / 3.10.0-beta.1
Code Snippets v3.10.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 / snippet-ops.php

snippet-ops.php in Code Snippets 3.10.0-beta.1, at php/snippet-ops.php

908 lines 26.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Functions to perform snippet operations
4 *
5 * @package Code_Snippets
6 */
7
8 namespace Code_Snippets;
9
10 use Code_Snippets\Core\DB;
11 use Code_Snippets\Flat_Files\Snippet_Files;
12 use Exception;
13 use Code_Snippets\Model\Snippet;
14 use Code_Snippets\Utils\Validator;
15 use Throwable;
16 use function Code_Snippets\Utils\get_self_option;
17 use function Code_Snippets\Utils\update_self_option;
18
19 /**
20 * Get the locked status for a snippet from wp_options.
21 *
22 * @param int $snippet_id Snippet ID.
23 * @param bool|null $network Whether the snippet is network-wide (true) or site-wide (false).
24 *
25 * @return bool Whether the snippet is locked.
26 */
27 function is_snippet_locked( int $snippet_id, ?bool $network = null ): bool {
28 $network = DB::validate_network_param( $network );
29 $locked_snippets = get_self_option( $network, 'code_snippets_locked', [] );
30
31 return isset( $locked_snippets[ $snippet_id ] ) && $locked_snippets[ $snippet_id ];
32 }
33
34 /**
35 * Set the locked status for a snippet in wp_options.
36 *
37 * @param int $snippet_id Snippet ID.
38 * @param bool $locked Whether the snippet should be locked.
39 * @param bool|null $network Whether the snippet is network-wide (true) or site-wide (false).
40 *
41 * @return void
42 */
43 function set_snippet_locked( int $snippet_id, bool $locked, ?bool $network = null ): void {
44 $network = DB::validate_network_param( $network );
45 $locked_snippets = get_self_option( $network, 'code_snippets_locked', [] );
46
47 if ( $locked ) {
48 $locked_snippets[ $snippet_id ] = true;
49 } else {
50 unset( $locked_snippets[ $snippet_id ] );
51 }
52
53 update_self_option( $network, 'code_snippets_locked', $locked_snippets );
54 }
55
56 /**
57 * Clean the cache where active snippets are stored.
58 *
59 * @param string $table_name Snippets table name.
60 * @param array<string>|false $scopes List of scopes. Optional. If not provided, will flush the cache for all scopes.
61 *
62 * @return void
63 */
64 function clean_active_snippets_cache( string $table_name, $scopes = false ) {
65 $scope_groups = $scopes
66 ? [ $scopes ]
67 : [
68 [ 'head-content', 'body-content', 'footer-content' ],
69 [ 'global', 'single-use', 'front-end' ],
70 [ 'global', 'single-use', 'admin' ],
71 ];
72
73 foreach ( $scope_groups as $scopes ) {
74 wp_cache_delete( sprintf( 'active_snippets_%s_%s', sanitize_key( join( '_', $scopes ) ), $table_name ), CACHE_GROUP );
75 }
76 }
77
78 /**
79 * Flush all snippets caches for a given database table.
80 *
81 * @param string $table_name Snippets table name.
82 *
83 * @return void
84 */
85 function clean_snippets_cache( string $table_name ) {
86 wp_cache_delete( "all_snippet_tags_$table_name", CACHE_GROUP );
87 wp_cache_delete( "all_snippets_$table_name", CACHE_GROUP );
88 clean_active_snippets_cache( $table_name );
89 }
90
91 /**
92 * Retrieve a list of snippets from the database.
93 * Read operation.
94 *
95 * @param array<string> $ids The IDs of the snippets to fetch.
96 * @param bool|null $network Retrieve multisite-wide snippets (true) or site-wide snippets (false).
97 *
98 * @return Snippet[] List of Snippet objects.
99 *
100 * @since 2.0
101 */
102 function get_snippets( array $ids = [], ?bool $network = null ): array {
103 global $wpdb;
104
105 // If only one ID has been passed in, defer to the get_snippet() function.
106 $ids_count = count( $ids );
107 if ( 1 === $ids_count ) {
108 return [ get_snippet( $ids[0], $network ) ];
109 }
110
111 $network = DB::validate_network_param( $network );
112 $table_name = code_snippets()->db->get_table_name( $network );
113
114 $snippets = wp_cache_get( "all_snippets_$table_name", CACHE_GROUP );
115
116 // Fetch all snippets from the database if none are cached.
117 if ( ! is_array( $snippets ) ) {
118 $results = $wpdb->get_results( "SELECT * FROM $table_name", ARRAY_A );
119
120 $snippets = $results
121 ? array_map(
122 function ( $snippet_data ) use ( $network ) {
123 $snippet_data['network'] = $network;
124 $snippet = new Snippet( $snippet_data );
125 // Load locked from wp_options.
126 if ( $snippet->id > 0 ) {
127 $snippet->locked = is_snippet_locked( $snippet->id, $network );
128 }
129 return $snippet;
130 },
131 $results
132 )
133 : [];
134
135 $snippets = apply_filters( 'code_snippets/get_snippets', $snippets, $network );
136
137 if ( 0 === $ids_count ) {
138 wp_cache_set( "all_snippets_$table_name", $snippets, CACHE_GROUP );
139 }
140 }
141
142 // If a list of IDs are provided, narrow down the snippets list.
143 if ( $ids_count > 0 ) {
144 $ids = array_map( 'intval', $ids );
145 return array_values(
146 array_filter(
147 $snippets,
148 function ( Snippet $snippet ) use ( $ids ) {
149 return in_array( $snippet->id, $ids, true );
150 }
151 )
152 );
153 }
154
155 return $snippets;
156 }
157
158 /**
159 * Gets all used tags from the database.
160 * Read operation.
161 *
162 * @since 2.0
163 */
164 function get_all_snippet_tags() {
165 global $wpdb;
166 $table_name = code_snippets()->db->get_table_name();
167 $cache_key = "all_snippet_tags_$table_name";
168
169 $tags = wp_cache_get( $cache_key, CACHE_GROUP );
170 if ( $tags ) {
171 return $tags;
172 }
173
174 // Grab all tags from the database.
175 $tags = array();
176 $all_tags = $wpdb->get_col( "SELECT tags FROM $table_name" );
177
178 // Merge all tags into a single array.
179 foreach ( $all_tags as $snippet_tags ) {
180 $snippet_tags = code_snippets_build_tags_array( $snippet_tags );
181 $tags = array_merge( $snippet_tags, $tags );
182 }
183
184 // Remove duplicate tags.
185 $tags = array_values( array_unique( $tags, SORT_REGULAR ) );
186 wp_cache_set( $cache_key, $tags, CACHE_GROUP );
187 return $tags;
188 }
189
190 /**
191 * Make sure that the tags are a valid array.
192 *
193 * @param array|string $tags The tags to convert into an array.
194 *
195 * @return array<string> The converted tags.
196 *
197 * @since 2.0.0
198 */
199 function code_snippets_build_tags_array( $tags ): array {
200
201 /* If there are no tags set, return an empty array. */
202 if ( empty( $tags ) ) {
203 return array();
204 }
205
206 /* If the tags are set as a string, convert them into an array. */
207 if ( is_string( $tags ) ) {
208 $tags = wp_strip_all_tags( $tags );
209 $tags = str_replace( ', ', ',', $tags );
210 $tags = explode( ',', $tags );
211 }
212
213 /* If we still don't have an array, just convert whatever we do have into one. */
214 return (array) $tags;
215 }
216
217 /**
218 * Retrieve a single snippets from the database.
219 * Will return empty snippet object if no snippet ID is specified.
220 * Read operation.
221 *
222 * @param int $id The ID of the snippet to retrieve. 0 to build a new snippet.
223 * @param bool|null $network Retrieve a multisite-wide snippet (true) or site-wide snippet (false).
224 *
225 * @return ?Snippet A single snippet object.
226 *
227 * @since 2.0.0
228 */
229 function get_snippet( int $id = 0, ?bool $network = null ): ?Snippet {
230 global $wpdb;
231
232 $id = absint( $id );
233 $network = DB::validate_network_param( $network );
234 $table_name = code_snippets()->db->get_table_name( $network );
235
236 if ( 0 === $id ) {
237 // If an invalid ID is provided, then return an empty snippet object.
238 $snippet = new Snippet();
239
240 } else {
241 $cached_snippets = wp_cache_get( "all_snippets_$table_name", CACHE_GROUP );
242
243 // Attempt to fetch snippet from the cached list, if it exists.
244 if ( is_array( $cached_snippets ) ) {
245 foreach ( $cached_snippets as $snippet ) {
246 if ( $snippet->id === $id ) {
247 return apply_filters( 'code_snippets/get_snippet', $snippet, $id, $network );
248 }
249 }
250 }
251
252 // Otherwise, retrieve the snippet from the database.
253 // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
254 $snippet_data = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id ) );
255 $snippet = new Snippet( $snippet_data );
256 }
257
258 $snippet->network = $network;
259
260 // Load locked from wp_options if snippet has an ID.
261 if ( $snippet->id > 0 ) {
262 $snippet->locked = is_snippet_locked( $snippet->id, $network );
263 }
264
265 return apply_filters( 'code_snippets/get_snippet', $snippet, $id, $network );
266 }
267
268
269 /**
270 * Ensure the list of shared network snippets is correct if one has been recently active or deactivated.
271 * Write operation.
272 *
273 * @access private
274 *
275 * @param Snippet[] $snippets Snippets that was recently updated.
276 *
277 * @return bool Whether an update was performed.
278 */
279 function update_shared_network_snippets( array $snippets ): bool {
280 $shared_ids = [];
281 $unshared_ids = [];
282
283 if ( ! is_multisite() ) {
284 return false;
285 }
286
287 foreach ( $snippets as $snippet ) {
288 if ( $snippet->network ) {
289 if ( $snippet->shared_network ) {
290 $shared_ids[] = $snippet->id;
291 } else {
292 $unshared_ids[] = $snippet->id;
293 }
294 }
295 }
296
297 if ( ! $shared_ids && ! $unshared_ids ) {
298 return false;
299 }
300
301 $existing_shared_ids = get_site_option( 'shared_network_snippets', [] );
302 $updated_shared_ids = array_values( array_diff( array_merge( $existing_shared_ids, $shared_ids ), $unshared_ids ) );
303
304 if ( $existing_shared_ids === $updated_shared_ids ) {
305 return false;
306 }
307
308 update_site_option( 'shared_network_snippets', $updated_shared_ids );
309
310 // Deactivate the snippet on all sites if necessary.
311 if ( $unshared_ids ) {
312 $sites = get_sites( [ 'fields' => 'ids' ] );
313
314 foreach ( $sites as $site ) {
315 switch_to_blog( $site );
316 $active_shared_ids = get_option( 'active_shared_network_snippets' );
317
318 if ( is_array( $active_shared_ids ) ) {
319 $active_shared_ids = array_diff( $active_shared_ids, $unshared_ids );
320 update_option( 'active_shared_network_snippets', $active_shared_ids );
321 }
322
323 clean_active_snippets_cache( code_snippets()->db->ms_table );
324 }
325
326 restore_current_blog();
327 }
328
329 return true;
330 }
331
332 /**
333 * Activates a snippet.
334 * Write operation.
335 *
336 * @param int $id ID of the snippet to activate.
337 * @param bool|null $network Whether the snippets are multisite-wide (true) or site-wide (false).
338 *
339 * @return Snippet|string Snippet object on success, error message on failure.
340 * @since 2.0.0
341 */
342 function activate_snippet( int $id, ?bool $network = null ) {
343 global $wpdb;
344 $network = DB::validate_network_param( $network );
345 $table_name = code_snippets()->db->get_table_name( $network );
346
347 // Retrieve the snippet code from the database for validation before activating.
348 $snippet = get_snippet( $id, $network );
349
350 if ( 0 === $snippet->id ) {
351 // translators: %d: snippet identifier.
352 return sprintf( __( 'Could not locate snippet with ID %d.', 'code-snippets' ), $id );
353 }
354
355 if ( 'php' === $snippet->type ) {
356 $validator = new Validator( $snippet->code );
357 if ( $validator->validate() ) {
358 return __( 'Could not activate snippet: code did not pass validation.', 'code-snippets' );
359 }
360 }
361
362 $result = $wpdb->update(
363 $table_name,
364 array( 'active' => '1' ),
365 array( 'id' => $id ),
366 array( '%d' ),
367 array( '%d' )
368 );
369
370 if ( ! $result ) {
371 return __( 'Could not activate snippet.', 'code-snippets' );
372 }
373
374 update_shared_network_snippets( [ $snippet ] );
375 do_action( 'code_snippets/activate_snippet', $snippet, $network );
376 clean_snippets_cache( $table_name );
377 return $snippet;
378 }
379
380 /**
381 * Activates multiple snippets.
382 * Write operation.
383 *
384 * @param array<int> $ids The IDs of the snippets to activate.
385 * @param bool|null $network Whether the snippets are multisite-wide (true) or site-wide (false).
386 *
387 * @return Snippet[]|null Snippets which were successfully activated, or null on failure.
388 *
389 * @since 2.0.0
390 */
391 function activate_snippets( array $ids, ?bool $network = null ): ?array {
392 global $wpdb;
393 $network = DB::validate_network_param( $network );
394 $table_name = code_snippets()->db->get_table_name( $network );
395
396 $snippets = get_snippets( $ids, $network );
397
398 if ( ! $snippets ) {
399 return null;
400 }
401
402 // Loop through each snippet code and validate individually.
403 $valid_ids = [];
404 $valid_snippets = [];
405
406 foreach ( $snippets as $snippet ) {
407 $validator = new Validator( $snippet->code );
408 $code_error = $validator->validate();
409
410 if ( ! $code_error ) {
411 $valid_ids[] = $snippet->id;
412 $valid_snippets[] = $snippet;
413 }
414 }
415
416 // If there are no valid snippets, then we're done.
417 if ( ! $valid_ids ) {
418 return null;
419 }
420
421 // Build a SQL query containing all IDs, as wpdb::update does not support OR conditionals.
422 $ids_format = implode( ',', array_fill( 0, count( $valid_ids ), '%d' ) );
423
424 // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
425 $rows_updated = $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET active = 1 WHERE id IN ($ids_format)", $valid_ids ) );
426
427 if ( ! $rows_updated ) {
428 return null;
429 }
430
431 update_shared_network_snippets( $valid_snippets );
432 do_action( 'code_snippets/activate_snippets', $valid_snippets, $table_name );
433 clean_snippets_cache( $table_name );
434 return $valid_ids;
435 }
436
437 /**
438 * Deactivate a snippet.
439 * Write operation.
440 *
441 * @param int $id ID of the snippet to deactivate.
442 * @param bool|null $network Whether the snippets are multisite-wide (true) or site-wide (false).
443 *
444 * @return Snippet|null Snippet that was deactivated on success, or null on failure.
445 *
446 * @since 2.0.0
447 */
448 function deactivate_snippet( int $id, ?bool $network = null ): ?Snippet {
449 global $wpdb;
450 $network = DB::validate_network_param( $network );
451 $table = code_snippets()->db->get_table_name( $network );
452
453 // Set the snippet to inactive.
454 $result = $wpdb->update(
455 $table,
456 array( 'active' => '0' ),
457 array( 'id' => $id ),
458 array( '%d' ),
459 array( '%d' )
460 );
461
462 if ( ! $result ) {
463 return null;
464 }
465
466 // Update the recently active list.
467 $snippet = get_snippet( $id );
468 $recently_active = [ $id => time() ] + get_self_option( $network, 'recently_active_snippets', [] );
469 update_self_option( $network, 'recently_active_snippets', $recently_active );
470
471 update_shared_network_snippets( [ $snippet ] );
472 do_action( 'code_snippets/deactivate_snippet', $id, $network );
473 clean_snippets_cache( $table );
474
475 return $snippet;
476 }
477
478 /**
479 * Deletes a snippet from the database.
480 * Write operation.
481 *
482 * @param int $id ID of the snippet to delete.
483 * @param bool|null $network Delete from network-wide (true) or site-wide (false) table.
484 *
485 * @return bool Whether the snippet was deleted successfully.
486 *
487 * @since 2.0.0
488 */
489 function delete_snippet( int $id, ?bool $network = null ): bool {
490 global $wpdb;
491 $network = DB::validate_network_param( $network );
492 $table = code_snippets()->db->get_table_name( $network );
493
494 $snippet = get_snippet( $id, $network );
495
496 // Prevent deletion of locked snippets.
497 if ( $snippet->locked ) {
498 return false;
499 }
500
501 $result = $wpdb->delete(
502 $table,
503 array( 'id' => $id ),
504 array( '%d' )
505 );
506
507 if ( $result ) {
508 do_action( 'code_snippets/delete_snippet', $snippet, $network );
509 clean_snippets_cache( $table );
510
511 $recently_active = get_self_option( $network, 'recently_active_snippets', [] );
512
513 if ( isset( $recently_active[ $id ] ) ) {
514 unset( $recently_active[ $id ] );
515 update_self_option( $network, 'recently_active_snippets', $recently_active );
516 }
517 }
518
519 return (bool) $result;
520 }
521
522 /**
523 * Trashes a snippet from the database.
524 * Write operation.
525 *
526 * @param int $id ID of the snippet to trash.
527 * @param bool|null $network Trash from network-wide (true) or site-wide (false) table.
528 *
529 * @return bool Whether the snippet was trashed successfully.
530 *
531 * @since 3.8.0
532 */
533 function trash_snippet( int $id, ?bool $network = null ): bool {
534 global $wpdb;
535 $network = DB::validate_network_param( $network );
536 $table = code_snippets()->db->get_table_name( $network );
537
538 $snippet = get_snippet( $id, $network );
539
540 // Prevent trashing of locked snippets.
541 if ( $snippet->locked ) {
542 return false;
543 }
544
545 $wpdb->update( $table, [ 'active' => '-1' ], [ 'id' => $id ], [ '%d' ] );
546
547 do_action( 'code_snippets/trash_snippet', $snippet, $network );
548 clean_snippets_cache( $table );
549
550 return true;
551 }
552
553 /**
554 * Restore a trashed snippet by setting its active status back to 0 (inactive).
555 * Write operation.
556 *
557 * @param int $id Snippet ID to restore.
558 * @param bool|null $network Whether the snippet is multisite-wide (true) or site-wide (false).
559 *
560 * @return bool Whether the restore was successful.
561 *
562 * @since 3.8.0
563 */
564 function restore_snippet( int $id, ?bool $network = null ): bool {
565 global $wpdb;
566 $network = DB::validate_network_param( $network );
567 $table = code_snippets()->db->get_table_name( $network );
568
569 $result = $wpdb->update( $table, [ 'active' => '0' ], [ 'id' => $id ], [ '%d' ] );
570
571 if ( $result ) {
572 do_action( 'code_snippets/restore_snippet', $id, $network );
573 clean_snippets_cache( $table );
574 }
575
576 return (bool) $result;
577 }
578
579 /**
580 * Test snippet code for errors, augmenting the snippet object.
581 *
582 * @param Snippet $snippet Snippet object.
583 */
584 function test_snippet_code( Snippet $snippet ) {
585 $snippet->code_error = null;
586 $snippet->code_error_trace = null;
587
588 if ( 'php' !== $snippet->type ) {
589 return;
590 }
591
592 $validator = new Validator( $snippet->code );
593 $result = $validator->validate();
594
595 if ( $result ) {
596 $snippet->code_error = [ $result['message'], $result['line'] ];
597 $snippet->code_error_trace = ( new Exception() )->getTraceAsString();
598 }
599
600 if ( ! $snippet->code_error && 'single-use' !== $snippet->scope ) {
601 $result = execute_snippet( $snippet->code, $snippet->id, true );
602
603 if ( $result instanceof Throwable ) {
604 $snippet->code_error = [
605 ucfirst( rtrim( $result->getMessage(), '.' ) ) . '.',
606 $result->getLine(),
607 ];
608 $snippet->code_error_trace = $result->getTraceAsString();
609 }
610 }
611 }
612
613 /**
614 * Saves a snippet to the database.
615 * Write operation.
616 *
617 * @param Snippet|array<string, mixed> $snippet The snippet to add/update to the database.
618 *
619 * @return Snippet|null Updated snippet.
620 *
621 * @since 2.0.0
622 */
623 function save_snippet( $snippet ): ?Snippet {
624 global $wpdb;
625 $table = code_snippets()->db->get_table_name( $snippet->network );
626
627 if ( ! $snippet instanceof Snippet ) {
628 $snippet = new Snippet( $snippet );
629 }
630
631 // Prevent modification of locked snippets (allow unlocking itself).
632 if ( 0 !== $snippet->id ) {
633 $old_snippet = get_snippet( $snippet->id, $snippet->network );
634
635 if ( $old_snippet->locked && $snippet->locked ) {
636 // If it was locked and the new request still wants it locked,
637 // prevent changes to sensitive fields (code and name).
638 $snippet->code = $old_snippet->code;
639 $snippet->name = $old_snippet->name;
640 }
641 }
642
643 // Update the last modification date if necessary.
644 $snippet->update_modified();
645
646 if ( 'php' === $snippet->type ) {
647 // Remove tags from beginning and end of snippet.
648 $snippet->code = preg_replace( '|^\s*<\?(php)?|', '', $snippet->code );
649 $snippet->code = preg_replace( '|\?>\s*$|', '', $snippet->code );
650
651 // Deactivate snippet if code contains errors.
652 if ( $snippet->active && 'single-use' !== $snippet->scope ) {
653 test_snippet_code( $snippet );
654
655 if ( $snippet->code_error ) {
656 $snippet->active = 0;
657 }
658 }
659 }
660
661 // Increment the revision number unless revision = 1 or revision is not set.
662 if ( $snippet->revision && $snippet->revision > 1 ) {
663 $snippet->increment_revision();
664 }
665
666 // Increment the revision number unless revision = 1 or revision is not set.
667 if ( $snippet->revision && $snippet->revision > 1 ) {
668 $snippet->increment_revision();
669 }
670
671 // Shared network snippets are always considered inactive.
672 $snippet->active = $snippet->active && ! $snippet->shared_network;
673
674 // Build the list of data to insert (excluding locked, which is stored in wp_options).
675 $data = [
676 'name' => $snippet->name,
677 'description' => $snippet->desc,
678 'code' => $snippet->code,
679 'tags' => $snippet->tags_list,
680 'scope' => $snippet->scope,
681 'condition_id' => intval( $snippet->condition_id ),
682 'priority' => $snippet->priority,
683 'active' => intval( $snippet->active ),
684 'modified' => $snippet->modified,
685 'revision' => $snippet->revision,
686 'cloud_id' => $snippet->cloud_id_owner ? $snippet->cloud_id_owner : null,
687 ];
688
689 // Create a new snippet if the ID is not set.
690 if ( 0 === $snippet->id ) {
691 $result = $wpdb->insert( $table, $data, '%s' );
692 if ( false === $result ) {
693 return null;
694 }
695
696 $snippet->id = $wpdb->insert_id;
697 $updated = get_snippet( $snippet->id, $snippet->network );
698 $updated->code_error = $snippet->code_error;
699 $updated->code_error_trace = $snippet->code_error_trace;
700 do_action( 'code_snippets/create_snippet', $updated, $table );
701
702 if ( $updated->id > 0 ) {
703 set_snippet_locked( $updated->id, $updated->locked, $updated->network );
704 }
705 } else {
706 // Otherwise, update the snippet data.
707 $existing = get_snippet( $snippet->id, $snippet->network );
708
709 set_snippet_locked( $snippet->id, $snippet->locked, $snippet->network );
710 $wpdb->update( $table, $data, [ 'id' => $snippet->id ], null, [ '%d' ] );
711
712 $updated = get_snippet( $snippet->id, $snippet->network );
713 $updated->code_error = $snippet->code_error;
714 $updated->code_error_trace = $snippet->code_error_trace;
715
716 do_action( 'code_snippets/update_snippet', $updated, $table, $existing, $snippet );
717
718 if ( ! $updated->active && $existing->active ) {
719 $recently_active = [ $updated->id => time() ] + get_self_option( $updated->network, 'recently_active_snippets', [] );
720 update_self_option( $updated->network, 'recently_active_snippets', $recently_active );
721 } elseif ( ! $updated->active ) {
722 $recently_active = get_self_option( $updated->network, 'recently_active_snippets', [] );
723
724 if ( isset( $recently_active[ $updated->id ] ) ) {
725 unset( $recently_active[ $updated->id ] );
726 update_self_option( $updated->network, 'recently_active_snippets', $recently_active );
727 }
728 }
729 }
730
731 update_shared_network_snippets( [ $updated ] );
732 clean_snippets_cache( $table );
733 return $updated;
734 }
735
736 /**
737 * Execute a snippet.
738 * Execute operation.
739 *
740 * Code must NOT be escaped, as it will be executed directly.
741 *
742 * @param string $code Snippet code to execute.
743 * @param int $id Snippet ID.
744 * @param bool $force Force snippet execution, even if save mode is active.
745 *
746 * @return Throwable|mixed Code error if encountered during execution, or result of snippet execution otherwise.
747 *
748 * @since 2.0.0
749 * @noinspection PhpUndefinedConstantInspection
750 *
751 * phpcs:disable Squiz.PHP.Eval.Discouraged
752 */
753 function execute_snippet( string $code, int $id = 0, bool $force = false ) {
754 /**
755 * Do not continue if safe mode is active.
756 *
757 * @noinspection PhpUndefinedConstantInspection
758 */
759 if ( empty( $code ) || ( ! $force && defined( 'CODE_SNIPPETS_SAFE_MODE' ) && CODE_SNIPPETS_SAFE_MODE ) ) {
760 return false;
761 }
762
763 ob_start();
764
765 try {
766 $result = eval( $code );
767 } catch ( Throwable $throwable ) {
768 $result = $throwable;
769 }
770
771 ob_end_clean();
772
773 do_action( 'code_snippets/after_execute_snippet', $code, $id, $result );
774 return $result;
775 }
776
777 /**
778 * Retrieve a single snippets from the database using its cloud ID.
779 *
780 * Read operation.
781 *
782 * @param string $cloud_id The Cloud ID of the snippet to retrieve.
783 * @param bool|null $multisite Retrieve a multisite-wide snippet (true) or site-wide snippet (false).
784 *
785 * @return Snippet|null A single snippet object or null if no snippet was found.
786 *
787 * @since 3.5.0
788 */
789 function get_snippet_by_cloud_id( string $cloud_id, ?bool $multisite = null ): ?Snippet {
790 global $wpdb;
791
792 $multisite = DB::validate_network_param( $multisite );
793 $table_name = code_snippets()->db->get_table_name( $multisite );
794
795 $cached_snippets = wp_cache_get( "all_snippets_$table_name", CACHE_GROUP );
796
797 // Attempt to fetch snippet from the cached list, if it exists.
798 if ( is_array( $cached_snippets ) ) {
799 foreach ( $cached_snippets as $snippet ) {
800 if ( $snippet->cloud_id === $cloud_id ) {
801 return apply_filters( 'code_snippets/get_snippet_by_cloud_id', $snippet, $cloud_id, $multisite );
802 }
803 }
804 }
805
806 // Otherwise, search for the snippet from the database.
807 $snippet_data = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE cloud_id = %s", $cloud_id ) ); // cache pass, db call ok.
808 $snippet = $snippet_data ? new Snippet( $snippet_data ) : null;
809
810 // Load locked from wp_options if snippet exists.
811 if ( $snippet && $snippet->id > 0 ) {
812 $snippet->network = $multisite;
813 $snippet->locked = is_snippet_locked( $snippet->id, $multisite );
814 }
815
816 return apply_filters( 'code_snippets/get_snippet_by_cloud_id', $snippet, $cloud_id, $multisite );
817 }
818
819 /**
820 * Update a snippet entry given a list of fields.
821 * Write operation.
822 *
823 * @param int $snippet_id ID of the snippet to update.
824 * @param array<string, mixed> $fields An array of fields mapped to their values.
825 * @param bool|null $network Update in network-wide (true) or site-wide (false) table.
826 */
827 function update_snippet_fields( int $snippet_id, array $fields, ?bool $network = null ) {
828 global $wpdb;
829
830 $network = DB::validate_network_param( $network );
831 $table = code_snippets()->db->get_table_name( $network );
832
833 // Build a new snippet object for the validation.
834 $snippet = new Snippet();
835 $snippet->id = $snippet_id;
836
837 // Validate fields through the snippet class and copy them into a clean array.
838 $clean_fields = array();
839 $locked_value = null;
840
841 foreach ( $fields as $field => $value ) {
842 // Handle locked separately (stored in wp_options).
843 if ( 'locked' === $field ) {
844 if ( $snippet->set_field( $field, $value ) ) {
845 $locked_value = $snippet->$field;
846 }
847 continue;
848 }
849
850 if ( $snippet->set_field( $field, $value ) ) {
851 $clean_fields[ $field ] = $snippet->$field;
852 }
853 }
854
855 // Update the snippet in the database (excluding locked).
856 if ( ! empty( $clean_fields ) ) {
857 $wpdb->update( $table, $clean_fields, array( 'id' => $snippet->id ), null, array( '%d' ) );
858 }
859
860 // Save locked to wp_options if it was provided.
861 if ( null !== $locked_value ) {
862 set_snippet_locked( $snippet->id, $locked_value, $network );
863 }
864
865 clean_snippets_cache( $table );
866 $updated = get_snippet( $snippet->id, $network );
867 if ( $updated->id ) {
868 do_action( 'code_snippets/update_snippet', $updated, $table );
869 }
870 }
871
872 /**
873 * Evaluate a snippet by loading it from the filesystem.
874 *
875 * @param string $code Snippet code.
876 * @param string $file Snippet filename.
877 * @param int $id Snippet ID.
878 * @param bool $force Force snippet execution, even if save mode is active.
879 *
880 * @return bool|Exception|Throwable|null Code error if encountered during execution, or result of snippet execution otherwise.
881 */
882 function execute_snippet_from_flat_file( string $code, string $file, int $id = 0, bool $force = false ) {
883 if ( ! is_file( $file ) ) {
884 execute_snippet( $code, $id, $force );
885 return true;
886 }
887
888 /* @noinspection PhpUndefinedConstantInspection */
889 if ( ! $force && defined( 'CODE_SNIPPETS_SAFE_MODE' ) && CODE_SNIPPETS_SAFE_MODE ) {
890 return false;
891 }
892
893 ob_start();
894
895 try {
896 require_once $file;
897 $result = null;
898 } catch ( Throwable $throwable ) {
899 $result = $throwable;
900 }
901
902 ob_end_clean();
903
904 do_action( 'code_snippets/after_execute_snippet_from_flat_file', $file, $id );
905
906 return $result ?? null;
907 }
908