PluginProbe
Code Snippets / 4.0.0-beta.1
Code Snippets v4.0.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 4.0.0-beta.1, at php/snippet-ops.php

904 lines 25.7 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 Exception;
12 use Code_Snippets\Model\Snippet;
13 use Code_Snippets\Utils\Validator;
14 use Throwable;
15 use function Code_Snippets\Utils\get_self_option;
16 use function Code_Snippets\Utils\update_self_option;
17
18 /**
19 * Get the locked status for a snippet from wp_options.
20 *
21 * @param int $snippet_id Snippet ID.
22 * @param bool|null $network Whether the snippet is network-wide (true) or site-wide (false).
23 *
24 * @return bool Whether the snippet is locked.
25 */
26 function is_snippet_locked( int $snippet_id, ?bool $network = null ): bool {
27 $network = DB::validate_network_param( $network );
28 $locked_snippets = get_self_option( $network, 'code_snippets_locked', [] );
29
30 return isset( $locked_snippets[ $snippet_id ] ) && $locked_snippets[ $snippet_id ];
31 }
32
33 /**
34 * Set the locked status for a snippet in wp_options.
35 *
36 * @param int $snippet_id Snippet ID.
37 * @param bool $locked Whether the snippet should be locked.
38 * @param bool|null $network Whether the snippet is network-wide (true) or site-wide (false).
39 *
40 * @return void
41 */
42 function set_snippet_locked( int $snippet_id, bool $locked, ?bool $network = null ): void {
43 $network = DB::validate_network_param( $network );
44 $locked_snippets = get_self_option( $network, 'code_snippets_locked', [] );
45
46 if ( $locked ) {
47 $locked_snippets[ $snippet_id ] = true;
48 } else {
49 unset( $locked_snippets[ $snippet_id ] );
50 }
51
52 update_self_option( $network, 'code_snippets_locked', $locked_snippets );
53 }
54
55 /**
56 * Clean the cache where active snippets are stored.
57 *
58 * @param string $table_name Snippets table name.
59 * @param array<string>|false $scopes List of scopes. Optional. If not provided, will flush the cache for all scopes.
60 *
61 * @return void
62 */
63 function clean_active_snippets_cache( string $table_name, $scopes = false ) {
64 $scope_groups = $scopes
65 ? [ $scopes ]
66 : [
67 [ 'head-content', 'body-content', 'footer-content' ],
68 [ 'global', 'single-use', 'front-end' ],
69 [ 'global', 'single-use', 'admin' ],
70 ];
71
72 foreach ( $scope_groups as $scopes ) {
73 wp_cache_delete( sprintf( 'active_snippets_%s_%s', sanitize_key( join( '_', $scopes ) ), $table_name ), CACHE_GROUP );
74 }
75 }
76
77 /**
78 * Flush all snippets caches for a given database table.
79 *
80 * @param string $table_name Snippets table name.
81 *
82 * @return void
83 */
84 function clean_snippets_cache( string $table_name ) {
85 wp_cache_delete( "all_snippet_tags_$table_name", CACHE_GROUP );
86 wp_cache_delete( "all_snippets_$table_name", CACHE_GROUP );
87 clean_active_snippets_cache( $table_name );
88 }
89
90 /**
91 * Retrieve a list of snippets from the database.
92 * Read operation.
93 *
94 * @param array<string> $ids The IDs of the snippets to fetch.
95 * @param bool|null $network Retrieve multisite-wide snippets (true) or site-wide snippets (false).
96 *
97 * @return Snippet[] List of Snippet objects.
98 *
99 * @since 2.0
100 */
101 function get_snippets( array $ids = array(), ?bool $network = null ): array {
102 global $wpdb;
103
104 // If only one ID has been passed in, defer to the get_snippet() function.
105 $ids_count = count( $ids );
106 if ( 1 === $ids_count ) {
107 return array( get_snippet( $ids[0], $network ) );
108 }
109
110 $network = DB::validate_network_param( $network );
111 $table_name = code_snippets()->db->get_table_name( $network );
112
113 $snippets = wp_cache_get( "all_snippets_$table_name", CACHE_GROUP );
114
115 // Fetch all snippets from the database if none are cached.
116 if ( ! is_array( $snippets ) ) {
117 $results = $wpdb->get_results( "SELECT * FROM $table_name", ARRAY_A );
118
119 $snippets = $results
120 ? array_map(
121 function ( $snippet_data ) use ( $network ) {
122 $snippet_data['network'] = $network;
123 $snippet = new Snippet( $snippet_data );
124 // Load locked from wp_options.
125 if ( $snippet->id > 0 ) {
126 $snippet->locked = is_snippet_locked( $snippet->id, $network );
127 }
128 return $snippet;
129 },
130 $results
131 )
132 : [];
133
134 $snippets = apply_filters( 'code_snippets/get_snippets', $snippets, $network );
135
136 if ( 0 === $ids_count ) {
137 wp_cache_set( "all_snippets_$table_name", $snippets, CACHE_GROUP );
138 }
139 }
140
141 // If a list of IDs are provided, narrow down the snippets list.
142 if ( $ids_count > 0 ) {
143 $ids = array_map( 'intval', $ids );
144 return array_values(
145 array_filter(
146 $snippets,
147 function ( Snippet $snippet ) use ( $ids ) {
148 return in_array( $snippet->id, $ids, true );
149 }
150 )
151 );
152 }
153
154 return $snippets;
155 }
156
157 /**
158 * Gets all used tags from the database.
159 * Read operation.
160 *
161 * @since 2.0
162 */
163 function get_all_snippet_tags() {
164 global $wpdb;
165 $table_name = code_snippets()->db->get_table_name();
166 $cache_key = "all_snippet_tags_$table_name";
167
168 $tags = wp_cache_get( $cache_key, CACHE_GROUP );
169 if ( $tags ) {
170 return $tags;
171 }
172
173 // Grab all tags from the database.
174 $tags = array();
175 $all_tags = $wpdb->get_col( "SELECT tags FROM $table_name" );
176
177 // Merge all tags into a single array.
178 foreach ( $all_tags as $snippet_tags ) {
179 $snippet_tags = code_snippets_build_tags_array( $snippet_tags );
180 $tags = array_merge( $snippet_tags, $tags );
181 }
182
183 // Remove duplicate tags.
184 $tags = array_values( array_unique( $tags, SORT_REGULAR ) );
185 wp_cache_set( $cache_key, $tags, CACHE_GROUP );
186 return $tags;
187 }
188
189 /**
190 * Make sure that the tags are a valid array.
191 *
192 * @param array|string $tags The tags to convert into an array.
193 *
194 * @return array<string> The converted tags.
195 *
196 * @since 2.0.0
197 */
198 function code_snippets_build_tags_array( $tags ): array {
199
200 /* If there are no tags set, return an empty array. */
201 if ( empty( $tags ) ) {
202 return array();
203 }
204
205 /* If the tags are set as a string, convert them into an array. */
206 if ( is_string( $tags ) ) {
207 $tags = wp_strip_all_tags( $tags );
208 $tags = str_replace( ', ', ',', $tags );
209 $tags = explode( ',', $tags );
210 }
211
212 /* If we still don't have an array, just convert whatever we do have into one. */
213 return (array) $tags;
214 }
215
216 /**
217 * Retrieve a single snippets from the database.
218 * Will return empty snippet object if no snippet ID is specified.
219 * Read operation.
220 *
221 * @param int $id The ID of the snippet to retrieve. 0 to build a new snippet.
222 * @param bool|null $network Retrieve a multisite-wide snippet (true) or site-wide snippet (false).
223 *
224 * @return Snippet A single snippet object.
225 *
226 * @since 2.0.0
227 */
228 function get_snippet( int $id = 0, ?bool $network = null ): Snippet {
229 global $wpdb;
230
231 $id = absint( $id );
232 $network = DB::validate_network_param( $network );
233 $table_name = code_snippets()->db->get_table_name( $network );
234
235 if ( 0 === $id ) {
236 // If an invalid ID is provided, then return an empty snippet object.
237 $snippet = new Snippet();
238
239 } else {
240 $cached_snippets = wp_cache_get( "all_snippets_$table_name", CACHE_GROUP );
241
242 // Attempt to fetch snippet from the cached list, if it exists.
243 if ( is_array( $cached_snippets ) ) {
244 foreach ( $cached_snippets as $snippet ) {
245 if ( $snippet->id === $id ) {
246 return apply_filters( 'code_snippets/get_snippet', $snippet, $id, $network );
247 }
248 }
249 }
250
251 // Otherwise, retrieve the snippet from the database.
252 // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
253 $snippet_data = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id ) );
254 $snippet = new Snippet( $snippet_data );
255 }
256
257 $snippet->network = $network;
258
259 // Load locked from wp_options if snippet has an ID.
260 if ( $snippet->id > 0 ) {
261 $snippet->locked = is_snippet_locked( $snippet->id, $network );
262 }
263
264 return apply_filters( 'code_snippets/get_snippet', $snippet, $id, $network );
265 }
266
267
268 /**
269 * Ensure the list of shared network snippets is correct if one has been recently active or deactivated.
270 * Write operation.
271 *
272 * @access private
273 *
274 * @param Snippet[] $snippets Snippets that was recently updated.
275 *
276 * @return bool Whether an update was performed.
277 */
278 function update_shared_network_snippets( array $snippets ): bool {
279 $shared_ids = [];
280 $unshared_ids = [];
281
282 if ( ! is_multisite() ) {
283 return false;
284 }
285
286 foreach ( $snippets as $snippet ) {
287 if ( $snippet->network ) {
288 if ( $snippet->shared_network ) {
289 $shared_ids[] = $snippet->id;
290 } else {
291 $unshared_ids[] = $snippet->id;
292 }
293 }
294 }
295
296 if ( ! $shared_ids && ! $unshared_ids ) {
297 return false;
298 }
299
300 $existing_shared_ids = get_site_option( 'shared_network_snippets', [] );
301 $updated_shared_ids = array_values( array_diff( array_merge( $existing_shared_ids, $shared_ids ), $unshared_ids ) );
302
303 if ( $existing_shared_ids === $updated_shared_ids ) {
304 return false;
305 }
306
307 update_site_option( 'shared_network_snippets', $updated_shared_ids );
308
309 // Deactivate the snippet on all sites if necessary.
310 if ( $unshared_ids ) {
311 $sites = get_sites( [ 'fields' => 'ids' ] );
312
313 foreach ( $sites as $site ) {
314 switch_to_blog( $site );
315 $active_shared_ids = get_option( 'active_shared_network_snippets' );
316
317 if ( is_array( $active_shared_ids ) ) {
318 $active_shared_ids = array_diff( $active_shared_ids, $unshared_ids );
319 update_option( 'active_shared_network_snippets', $active_shared_ids );
320 }
321
322 clean_active_snippets_cache( code_snippets()->db->ms_table );
323 }
324
325 restore_current_blog();
326 }
327
328 return true;
329 }
330
331 /**
332 * Activates a snippet.
333 * Write operation.
334 *
335 * @param int $id ID of the snippet to activate.
336 * @param bool|null $network Whether the snippets are multisite-wide (true) or site-wide (false).
337 *
338 * @return Snippet|string Snippet object on success, error message on failure.
339 * @since 2.0.0
340 */
341 function activate_snippet( int $id, ?bool $network = null ) {
342 global $wpdb;
343 $network = DB::validate_network_param( $network );
344 $table_name = code_snippets()->db->get_table_name( $network );
345
346 // Retrieve the snippet code from the database for validation before activating.
347 $snippet = get_snippet( $id, $network );
348
349 if ( 0 === $snippet->id ) {
350 // translators: %d: snippet identifier.
351 return sprintf( __( 'Could not locate snippet with ID %d.', 'code-snippets' ), $id );
352 }
353
354 if ( 'php' === $snippet->type ) {
355 $validator = new Validator( $snippet->code );
356 if ( $validator->validate() ) {
357 return __( 'Could not activate snippet: code did not pass validation.', 'code-snippets' );
358 }
359 }
360
361 $result = $wpdb->update(
362 $table_name,
363 array( 'active' => '1' ),
364 array( 'id' => $id ),
365 array( '%d' ),
366 array( '%d' )
367 );
368
369 if ( ! $result ) {
370 return __( 'Could not activate snippet.', 'code-snippets' );
371 }
372
373 update_shared_network_snippets( [ $snippet ] );
374 do_action( 'code_snippets/activate_snippet', $snippet, $network );
375 clean_snippets_cache( $table_name );
376 return $snippet;
377 }
378
379 /**
380 * Activates multiple snippets.
381 * Write operation.
382 *
383 * @param array<int> $ids The IDs of the snippets to activate.
384 * @param bool|null $network Whether the snippets are multisite-wide (true) or site-wide (false).
385 *
386 * @return Snippet[]|null Snippets which were successfully activated, or null on failure.
387 *
388 * @since 2.0.0
389 */
390 function activate_snippets( array $ids, ?bool $network = null ): ?array {
391 global $wpdb;
392 $network = DB::validate_network_param( $network );
393 $table_name = code_snippets()->db->get_table_name( $network );
394
395 $snippets = get_snippets( $ids, $network );
396
397 if ( ! $snippets ) {
398 return null;
399 }
400
401 // Loop through each snippet code and validate individually.
402 $valid_ids = [];
403 $valid_snippets = [];
404
405 foreach ( $snippets as $snippet ) {
406 $validator = new Validator( $snippet->code );
407 $code_error = $validator->validate();
408
409 if ( ! $code_error ) {
410 $valid_ids[] = $snippet->id;
411 $valid_snippets[] = $snippet;
412 }
413 }
414
415 // If there are no valid snippets, then we're done.
416 if ( ! $valid_ids ) {
417 return null;
418 }
419
420 // Build a SQL query containing all IDs, as wpdb::update does not support OR conditionals.
421 $ids_format = implode( ',', array_fill( 0, count( $valid_ids ), '%d' ) );
422
423 // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
424 $rows_updated = $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET active = 1 WHERE id IN ($ids_format)", $valid_ids ) );
425
426 if ( ! $rows_updated ) {
427 return null;
428 }
429
430 update_shared_network_snippets( $valid_snippets );
431 do_action( 'code_snippets/activate_snippets', $valid_snippets, $table_name );
432 clean_snippets_cache( $table_name );
433 return $valid_ids;
434 }
435
436 /**
437 * Deactivate a snippet.
438 * Write operation.
439 *
440 * @param int $id ID of the snippet to deactivate.
441 * @param bool|null $network Whether the snippets are multisite-wide (true) or site-wide (false).
442 *
443 * @return Snippet|null Snippet that was deactivated on success, or null on failure.
444 *
445 * @since 2.0.0
446 */
447 function deactivate_snippet( int $id, ?bool $network = null ): ?Snippet {
448 global $wpdb;
449 $network = DB::validate_network_param( $network );
450 $table = code_snippets()->db->get_table_name( $network );
451
452 // Set the snippet to inactive.
453 $result = $wpdb->update(
454 $table,
455 array( 'active' => '0' ),
456 array( 'id' => $id ),
457 array( '%d' ),
458 array( '%d' )
459 );
460
461 if ( ! $result ) {
462 return null;
463 }
464
465 // Update the recently active list.
466 $snippet = get_snippet( $id );
467 $recently_active = [ $id => time() ] + get_self_option( $network, 'recently_active_snippets', [] );
468 update_self_option( $network, 'recently_active_snippets', $recently_active );
469
470 update_shared_network_snippets( [ $snippet ] );
471 do_action( 'code_snippets/deactivate_snippet', $id, $network );
472 clean_snippets_cache( $table );
473
474 return $snippet;
475 }
476
477 /**
478 * Deletes a snippet from the database.
479 * Write operation.
480 *
481 * @param int $id ID of the snippet to delete.
482 * @param bool|null $network Delete from network-wide (true) or site-wide (false) table.
483 *
484 * @return bool Whether the snippet was deleted successfully.
485 *
486 * @since 2.0.0
487 */
488 function delete_snippet( int $id, ?bool $network = null ): bool {
489 global $wpdb;
490 $network = DB::validate_network_param( $network );
491 $table = code_snippets()->db->get_table_name( $network );
492
493 $snippet = get_snippet( $id, $network );
494
495 // Prevent deletion of locked snippets.
496 if ( $snippet->locked ) {
497 return false;
498 }
499
500 $result = $wpdb->delete(
501 $table,
502 array( 'id' => $id ),
503 array( '%d' )
504 );
505
506 if ( $result ) {
507 do_action( 'code_snippets/delete_snippet', $snippet, $network );
508 clean_snippets_cache( $table );
509 code_snippets()->cloud_api->delete_snippet_from_transient_data( $id );
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 code_snippets()->cloud_api->delete_snippet_from_transient_data( $id );
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 ? $snippet->cloud_id : 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 );
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 = [ $updated->id => time() ] + get_self_option( $updated->network, 'recently_active_snippets', [] );
716 update_self_option( $updated->network, 'recently_active_snippets', $recently_active );
717 } elseif ( ! $updated->active ) {
718 $recently_active = get_self_option( $updated->network, 'recently_active_snippets', [] );
719
720 if ( isset( $recently_active[ $updated->id ] ) ) {
721 unset( $recently_active[ $updated->id ] );
722 update_self_option( $updated->network, 'recently_active_snippets', $recently_active );
723 }
724 }
725 }
726
727 update_shared_network_snippets( [ $updated ] );
728 clean_snippets_cache( $table );
729 return $updated;
730 }
731
732 /**
733 * Execute a snippet.
734 * Execute operation.
735 *
736 * Code must NOT be escaped, as it will be executed directly.
737 *
738 * @param string $code Snippet code to execute.
739 * @param int $id Snippet ID.
740 * @param bool $force Force snippet execution, even if save mode is active.
741 *
742 * @return Throwable|mixed Code error if encountered during execution, or result of snippet execution otherwise.
743 *
744 * @since 2.0.0
745 * @noinspection PhpUndefinedConstantInspection
746 *
747 * phpcs:disable Squiz.PHP.Eval.Discouraged
748 */
749 function execute_snippet( string $code, int $id = 0, bool $force = false ) {
750 /**
751 * Do not continue if safe mode is active.
752 *
753 * @noinspection PhpUndefinedConstantInspection
754 */
755 if ( empty( $code ) || ( ! $force && defined( 'CODE_SNIPPETS_SAFE_MODE' ) && CODE_SNIPPETS_SAFE_MODE ) ) {
756 return false;
757 }
758
759 ob_start();
760
761 try {
762 $result = eval( $code );
763 } catch ( Throwable $throwable ) {
764 $result = $throwable;
765 }
766
767 ob_end_clean();
768
769 do_action( 'code_snippets/after_execute_snippet', $code, $id, $result );
770 return $result;
771 }
772
773 /**
774 * Retrieve a single snippets from the database using its cloud ID.
775 *
776 * Read operation.
777 *
778 * @param string $cloud_id The Cloud ID of the snippet to retrieve.
779 * @param bool|null $multisite Retrieve a multisite-wide snippet (true) or site-wide snippet (false).
780 *
781 * @return Snippet|null A single snippet object or null if no snippet was found.
782 *
783 * @since 3.5.0
784 */
785 function get_snippet_by_cloud_id( string $cloud_id, ?bool $multisite = null ): ?Snippet {
786 global $wpdb;
787
788 $multisite = DB::validate_network_param( $multisite );
789 $table_name = code_snippets()->db->get_table_name( $multisite );
790
791 $cached_snippets = wp_cache_get( "all_snippets_$table_name", CACHE_GROUP );
792
793 // Attempt to fetch snippet from the cached list, if it exists.
794 if ( is_array( $cached_snippets ) ) {
795 foreach ( $cached_snippets as $snippet ) {
796 if ( $snippet->cloud_id === $cloud_id ) {
797 return apply_filters( 'code_snippets/get_snippet_by_cloud_id', $snippet, $cloud_id, $multisite );
798 }
799 }
800 }
801
802 // Otherwise, search for the snippet from the database.
803 $snippet_data = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE cloud_id = %s", $cloud_id ) ); // cache pass, db call ok.
804 $snippet = $snippet_data ? new Snippet( $snippet_data ) : null;
805
806 // Load locked from wp_options if snippet exists.
807 if ( $snippet && $snippet->id > 0 ) {
808 $snippet->network = $multisite;
809 $snippet->locked = is_snippet_locked( $snippet->id, $multisite );
810 }
811
812 return apply_filters( 'code_snippets/get_snippet_by_cloud_id', $snippet, $cloud_id, $multisite );
813 }
814
815 /**
816 * Update a snippet entry given a list of fields.
817 * Write operation.
818 *
819 * @param int $snippet_id ID of the snippet to update.
820 * @param array<string, mixed> $fields An array of fields mapped to their values.
821 * @param bool|null $network Update in network-wide (true) or site-wide (false) table.
822 */
823 function update_snippet_fields( int $snippet_id, array $fields, ?bool $network = null ) {
824 global $wpdb;
825
826 $table = code_snippets()->db->get_table_name( $network );
827
828 // Build a new snippet object for the validation.
829 $snippet = new Snippet();
830 $snippet->id = $snippet_id;
831
832 // Validate fields through the snippet class and copy them into a clean array.
833 $clean_fields = array();
834 $locked_value = null;
835
836 foreach ( $fields as $field => $value ) {
837 // Handle locked separately (stored in wp_options).
838 if ( 'locked' === $field ) {
839 if ( $snippet->set_field( $field, $value ) ) {
840 $locked_value = $snippet->$field;
841 }
842 continue;
843 }
844
845 if ( $snippet->set_field( $field, $value ) ) {
846 $clean_fields[ $field ] = $snippet->$field;
847 }
848 }
849
850 // Update the snippet in the database (excluding locked).
851 if ( ! empty( $clean_fields ) ) {
852 $wpdb->update( $table, $clean_fields, array( 'id' => $snippet->id ), null, array( '%d' ) );
853 }
854
855 // Save locked to wp_options if it was provided.
856 if ( null !== $locked_value ) {
857 set_snippet_locked( $snippet->id, $locked_value, $network );
858 }
859
860 $updated = get_snippet( $snippet->id, $network );
861 if ( $updated->id ) {
862 do_action( 'code_snippets/update_snippet', $updated, $table );
863 }
864
865 clean_snippets_cache( $table );
866 }
867
868 /**
869 * Evaluate a snippet by loading it from the filesystem.
870 *
871 * @param string $code Snippet code.
872 * @param string $file Snippet filename.
873 * @param int $id Snippet ID.
874 * @param bool $force Force snippet execution, even if save mode is active.
875 *
876 * @return bool|Throwable|null
877 */
878 function execute_snippet_from_flat_file( string $code, string $file, int $id = 0, bool $force = false ) {
879 if ( ! is_file( $file ) ) {
880 execute_snippet( $code, $id, $force );
881 return true;
882 }
883
884 /* @noinspection PhpUndefinedConstantInspection */
885 if ( ! $force && defined( 'CODE_SNIPPETS_SAFE_MODE' ) && CODE_SNIPPETS_SAFE_MODE ) {
886 return false;
887 }
888
889 ob_start();
890
891 try {
892 require_once $file;
893 $result = null;
894 } catch ( Throwable $throwable ) {
895 $result = $throwable;
896 }
897
898 ob_end_clean();
899
900 do_action( 'code_snippets/after_execute_snippet_from_flat_file', $file, $id );
901
902 return $result ?? null;
903 }
904