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

snippet-ops.php in Code Snippets 3.10.1, at php/snippet-ops.php

905 lines 25.8 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 = get_self_option( $network, 'recently_active_snippets', [] );
469 $recently_active[ $id ] = time();
470 update_self_option( $network, 'recently_active_snippets', $recently_active );
471
472 update_shared_network_snippets( [ $snippet ] );
473 do_action( 'code_snippets/deactivate_snippet', $id, $network );
474 clean_snippets_cache( $table );
475
476 return $snippet;
477 }
478
479 /**
480 * Deletes a snippet from the database.
481 * Write operation.
482 *
483 * @param int $id ID of the snippet to delete.
484 * @param bool|null $network Delete from network-wide (true) or site-wide (false) table.
485 *
486 * @return bool Whether the snippet was deleted successfully.
487 *
488 * @since 2.0.0
489 */
490 function delete_snippet( int $id, ?bool $network = null ): bool {
491 global $wpdb;
492 $network = DB::validate_network_param( $network );
493 $table = code_snippets()->db->get_table_name( $network );
494
495 $snippet = get_snippet( $id, $network );
496
497 // Prevent deletion of locked snippets.
498 if ( $snippet->locked ) {
499 return false;
500 }
501
502 $result = $wpdb->delete(
503 $table,
504 array( 'id' => $id ),
505 array( '%d' )
506 );
507
508 if ( $result ) {
509 do_action( 'code_snippets/delete_snippet', $snippet, $network );
510 clean_snippets_cache( $table );
511
512 $recently_active = get_self_option( $network, 'recently_active_snippets', [] );
513
514 if ( isset( $recently_active[ $id ] ) ) {
515 unset( $recently_active[ $id ] );
516 update_self_option( $network, 'recently_active_snippets', $recently_active );
517 }
518 }
519
520 return (bool) $result;
521 }
522
523 /**
524 * Trashes a snippet from the database.
525 * Write operation.
526 *
527 * @param int $id ID of the snippet to trash.
528 * @param bool|null $network Trash from network-wide (true) or site-wide (false) table.
529 *
530 * @return bool Whether the snippet was trashed successfully.
531 *
532 * @since 3.8.0
533 */
534 function trash_snippet( int $id, ?bool $network = null ): bool {
535 global $wpdb;
536 $network = DB::validate_network_param( $network );
537 $table = code_snippets()->db->get_table_name( $network );
538
539 $snippet = get_snippet( $id, $network );
540
541 // Prevent trashing of locked snippets.
542 if ( $snippet->locked ) {
543 return false;
544 }
545
546 $wpdb->update( $table, [ 'active' => '-1' ], [ 'id' => $id ], [ '%d' ] );
547
548 do_action( 'code_snippets/trash_snippet', $snippet, $network );
549 clean_snippets_cache( $table );
550
551 return true;
552 }
553
554 /**
555 * Restore a trashed snippet by setting its active status back to 0 (inactive).
556 * Write operation.
557 *
558 * @param int $id Snippet ID to restore.
559 * @param bool|null $network Whether the snippet is multisite-wide (true) or site-wide (false).
560 *
561 * @return bool Whether the restore was successful.
562 *
563 * @since 3.8.0
564 */
565 function restore_snippet( int $id, ?bool $network = null ): bool {
566 global $wpdb;
567 $network = DB::validate_network_param( $network );
568 $table = code_snippets()->db->get_table_name( $network );
569
570 $result = $wpdb->update( $table, [ 'active' => '0' ], [ 'id' => $id ], [ '%d' ] );
571
572 if ( $result ) {
573 do_action( 'code_snippets/restore_snippet', $id, $network );
574 clean_snippets_cache( $table );
575 }
576
577 return (bool) $result;
578 }
579
580 /**
581 * Test snippet code for errors, augmenting the snippet object.
582 *
583 * @param Snippet $snippet Snippet object.
584 */
585 function test_snippet_code( Snippet $snippet ) {
586 $snippet->code_error = null;
587 $snippet->code_error_trace = null;
588
589 if ( 'php' !== $snippet->type ) {
590 return;
591 }
592
593 $validator = new Validator( $snippet->code );
594 $result = $validator->validate();
595
596 if ( $result ) {
597 $snippet->code_error = [ $result['message'], $result['line'] ];
598 $snippet->code_error_trace = ( new Exception() )->getTraceAsString();
599 }
600
601 if ( ! $snippet->code_error && 'single-use' !== $snippet->scope ) {
602 $result = execute_snippet( $snippet->code, $snippet->id, true );
603
604 if ( $result instanceof Throwable ) {
605 $snippet->code_error = [
606 ucfirst( rtrim( $result->getMessage(), '.' ) ) . '.',
607 $result->getLine(),
608 ];
609 $snippet->code_error_trace = $result->getTraceAsString();
610 }
611 }
612 }
613
614 /**
615 * Saves a snippet to the database.
616 * Write operation.
617 *
618 * @param Snippet|array<string, mixed> $snippet The snippet to add/update to the database.
619 *
620 * @return Snippet|null Updated snippet.
621 *
622 * @since 2.0.0
623 */
624 function save_snippet( $snippet ): ?Snippet {
625 global $wpdb;
626 $table = code_snippets()->db->get_table_name( $snippet->network );
627
628 if ( ! $snippet instanceof Snippet ) {
629 $snippet = new Snippet( $snippet );
630 }
631
632 // Prevent modification of locked snippets (allow unlocking itself).
633 if ( 0 !== $snippet->id ) {
634 $old_snippet = get_snippet( $snippet->id, $snippet->network );
635
636 if ( $old_snippet->locked && $snippet->locked ) {
637 // If it was locked and the new request still wants it locked,
638 // prevent changes to sensitive fields (code and name).
639 $snippet->code = $old_snippet->code;
640 $snippet->name = $old_snippet->name;
641 }
642 }
643
644 // Update the last modification date if necessary.
645 $snippet->update_modified();
646
647 if ( 'php' === $snippet->type ) {
648 // Remove tags from beginning and end of snippet.
649 $snippet->code = preg_replace( '|^\s*<\?(php)?|', '', $snippet->code );
650 $snippet->code = preg_replace( '|\?>\s*$|', '', $snippet->code );
651
652 // Deactivate snippet if code contains errors.
653 if ( $snippet->active && 'single-use' !== $snippet->scope ) {
654 test_snippet_code( $snippet );
655
656 if ( $snippet->code_error ) {
657 $snippet->active = 0;
658 }
659 }
660 }
661
662 // Increment the revision number unless revision = 1 or revision is not set.
663 if ( $snippet->revision && $snippet->revision > 1 ) {
664 $snippet->increment_revision();
665 }
666
667 // Shared network snippets are always considered inactive.
668 $snippet->active = $snippet->active && ! $snippet->shared_network;
669
670 // Build the list of data to insert (excluding locked, which is stored in wp_options).
671 $data = [
672 'name' => $snippet->name,
673 'description' => $snippet->desc,
674 'code' => $snippet->code,
675 'tags' => $snippet->tags_list,
676 'scope' => $snippet->scope,
677 'condition_id' => intval( $snippet->condition_id ),
678 'priority' => $snippet->priority,
679 'active' => intval( $snippet->active ),
680 'modified' => $snippet->modified,
681 'revision' => $snippet->revision,
682 'cloud_id' => $snippet->cloud_id_owner ? $snippet->cloud_id_owner : null,
683 ];
684
685 // Create a new snippet if the ID is not set.
686 if ( 0 === $snippet->id ) {
687 $result = $wpdb->insert( $table, $data, '%s' );
688 if ( false === $result ) {
689 return null;
690 }
691
692 $snippet->id = $wpdb->insert_id;
693 $updated = get_snippet( $snippet->id, $snippet->network );
694 $updated->code_error = $snippet->code_error;
695 $updated->code_error_trace = $snippet->code_error_trace;
696 do_action( 'code_snippets/create_snippet', $updated, $table );
697
698 if ( $updated->id > 0 ) {
699 set_snippet_locked( $updated->id, $updated->locked, $updated->network );
700 }
701 } else {
702 // Otherwise, update the snippet data.
703 $existing = get_snippet( $snippet->id, $snippet->network );
704
705 set_snippet_locked( $snippet->id, $snippet->locked, $snippet->network );
706 $wpdb->update( $table, $data, [ 'id' => $snippet->id ], null, [ '%d' ] );
707
708 $updated = get_snippet( $snippet->id, $snippet->network );
709 $updated->code_error = $snippet->code_error;
710 $updated->code_error_trace = $snippet->code_error_trace;
711
712 do_action( 'code_snippets/update_snippet', $updated, $table, $existing, $snippet );
713
714 if ( ! $updated->active && $existing->active ) {
715 $recently_active = get_self_option( $updated->network, 'recently_active_snippets', [] );
716 $recently_active[ $updated->id ] = time();
717 update_self_option( $updated->network, 'recently_active_snippets', $recently_active );
718 } elseif ( ! $updated->active ) {
719 $recently_active = get_self_option( $updated->network, 'recently_active_snippets', [] );
720
721 if ( isset( $recently_active[ $updated->id ] ) ) {
722 unset( $recently_active[ $updated->id ] );
723 update_self_option( $updated->network, 'recently_active_snippets', $recently_active );
724 }
725 }
726 }
727
728 update_shared_network_snippets( [ $updated ] );
729 clean_snippets_cache( $table );
730 return $updated;
731 }
732
733 /**
734 * Execute a snippet.
735 * Execute operation.
736 *
737 * Code must NOT be escaped, as it will be executed directly.
738 *
739 * @param string $code Snippet code to execute.
740 * @param int $id Snippet ID.
741 * @param bool $force Force snippet execution, even if save mode is active.
742 *
743 * @return Throwable|mixed Code error if encountered during execution, or result of snippet execution otherwise.
744 *
745 * @since 2.0.0
746 * @noinspection PhpUndefinedConstantInspection
747 *
748 * phpcs:disable Squiz.PHP.Eval.Discouraged
749 */
750 function execute_snippet( string $code, int $id = 0, bool $force = false ) {
751 /**
752 * Do not continue if safe mode is active.
753 *
754 * @noinspection PhpUndefinedConstantInspection
755 */
756 if ( empty( $code ) || ( ! $force && defined( 'CODE_SNIPPETS_SAFE_MODE' ) && CODE_SNIPPETS_SAFE_MODE ) ) {
757 return false;
758 }
759
760 ob_start();
761
762 try {
763 $result = eval( $code );
764 } catch ( Throwable $throwable ) {
765 $result = $throwable;
766 }
767
768 ob_end_clean();
769
770 do_action( 'code_snippets/after_execute_snippet', $code, $id, $result );
771 return $result;
772 }
773
774 /**
775 * Retrieve a single snippets from the database using its cloud ID.
776 *
777 * Read operation.
778 *
779 * @param string $cloud_id The Cloud ID of the snippet to retrieve.
780 * @param bool|null $multisite Retrieve a multisite-wide snippet (true) or site-wide snippet (false).
781 *
782 * @return Snippet|null A single snippet object or null if no snippet was found.
783 *
784 * @since 3.5.0
785 */
786 function get_snippet_by_cloud_id( string $cloud_id, ?bool $multisite = null ): ?Snippet {
787 global $wpdb;
788
789 $multisite = DB::validate_network_param( $multisite );
790 $table_name = code_snippets()->db->get_table_name( $multisite );
791
792 $cached_snippets = wp_cache_get( "all_snippets_$table_name", CACHE_GROUP );
793
794 // Attempt to fetch snippet from the cached list, if it exists.
795 if ( is_array( $cached_snippets ) ) {
796 foreach ( $cached_snippets as $snippet ) {
797 if ( $snippet->cloud_id === $cloud_id ) {
798 return apply_filters( 'code_snippets/get_snippet_by_cloud_id', $snippet, $cloud_id, $multisite );
799 }
800 }
801 }
802
803 // Otherwise, search for the snippet from the database.
804 $snippet_data = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE cloud_id = %s", $cloud_id ) ); // cache pass, db call ok.
805 $snippet = $snippet_data ? new Snippet( $snippet_data ) : null;
806
807 // Load locked from wp_options if snippet exists.
808 if ( $snippet && $snippet->id > 0 ) {
809 $snippet->network = $multisite;
810 $snippet->locked = is_snippet_locked( $snippet->id, $multisite );
811 }
812
813 return apply_filters( 'code_snippets/get_snippet_by_cloud_id', $snippet, $cloud_id, $multisite );
814 }
815
816 /**
817 * Update a snippet entry given a list of fields.
818 * Write operation.
819 *
820 * @param int $snippet_id ID of the snippet to update.
821 * @param array<string, mixed> $fields An array of fields mapped to their values.
822 * @param bool|null $network Update in network-wide (true) or site-wide (false) table.
823 */
824 function update_snippet_fields( int $snippet_id, array $fields, ?bool $network = null ) {
825 global $wpdb;
826
827 $network = DB::validate_network_param( $network );
828 $table = code_snippets()->db->get_table_name( $network );
829
830 // Build a new snippet object for the validation.
831 $snippet = new Snippet();
832 $snippet->id = $snippet_id;
833
834 // Validate fields through the snippet class and copy them into a clean array.
835 $clean_fields = array();
836 $locked_value = null;
837
838 foreach ( $fields as $field => $value ) {
839 // Handle locked separately (stored in wp_options).
840 if ( 'locked' === $field ) {
841 if ( $snippet->set_field( $field, $value ) ) {
842 $locked_value = $snippet->$field;
843 }
844 continue;
845 }
846
847 if ( $snippet->set_field( $field, $value ) ) {
848 $clean_fields[ $field ] = $snippet->$field;
849 }
850 }
851
852 // Update the snippet in the database (excluding locked).
853 if ( ! empty( $clean_fields ) ) {
854 $wpdb->update( $table, $clean_fields, array( 'id' => $snippet->id ), null, array( '%d' ) );
855 }
856
857 // Save locked to wp_options if it was provided.
858 if ( null !== $locked_value ) {
859 set_snippet_locked( $snippet->id, $locked_value, $network );
860 }
861
862 clean_snippets_cache( $table );
863 $updated = get_snippet( $snippet->id, $network );
864 if ( $updated->id ) {
865 do_action( 'code_snippets/update_snippet', $updated, $table );
866 }
867 }
868
869 /**
870 * Evaluate a snippet by loading it from the filesystem.
871 *
872 * @param string $code Snippet code.
873 * @param string $file Snippet filename.
874 * @param int $id Snippet ID.
875 * @param bool $force Force snippet execution, even if save mode is active.
876 *
877 * @return bool|Exception|Throwable|null Code error if encountered during execution, or result of snippet execution otherwise.
878 */
879 function execute_snippet_from_flat_file( string $code, string $file, int $id = 0, bool $force = false ) {
880 if ( ! is_file( $file ) ) {
881 execute_snippet( $code, $id, $force );
882 return true;
883 }
884
885 /* @noinspection PhpUndefinedConstantInspection */
886 if ( ! $force && defined( 'CODE_SNIPPETS_SAFE_MODE' ) && CODE_SNIPPETS_SAFE_MODE ) {
887 return false;
888 }
889
890 ob_start();
891
892 try {
893 require_once $file;
894 $result = null;
895 } catch ( Throwable $throwable ) {
896 $result = $throwable;
897 }
898
899 ob_end_clean();
900
901 do_action( 'code_snippets/after_execute_snippet_from_flat_file', $file, $id );
902
903 return $result ?? null;
904 }
905