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 / Flat_Files / Snippet_Files.php

Snippet_Files.php in Code Snippets 3.10.1, at php/Flat_Files/Snippet_Files.php

708 lines 20.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Code_Snippets\Flat_Files;
4
5 use Code_Snippets\Core\DB;
6 use Code_Snippets\Flat_Files\Interfaces\Filesystem_Adapter;
7 use Code_Snippets\Flat_Files\Interfaces\Snippet_Config_Repository;
8 use Code_Snippets\Flat_Files\Interfaces\Snippet_Type_Handler;
9 use Code_Snippets\Model\Snippet;
10 use function Code_Snippets\code_snippets;
11 use function Code_Snippets\get_snippet;
12 use function wp_hash;
13 use const Code_Snippets\CACHE_GROUP;
14
15 /**
16 /**
17 * Manage file-based snippet execution.
18 *
19 * Responsible for writing snippet code to disk, maintaining per-table config indexes,
20 * and retrieving the active snippet list from those config files.
21 */
22 class Snippet_Files {
23
24 /**
25 * Flag file name that indicates flat files are enabled.
26 */
27 private const ENABLED_FLAG_FILE = 'flat-files-enabled.flag';
28
29 /**
30 * Instance of handler registry.
31 *
32 * @var Handler_Registry
33 */
34 private Handler_Registry $handler_registry;
35
36 /**
37 * Instance of filesystem adapter.
38 *
39 * @var Filesystem_Adapter
40 */
41 private Filesystem_Adapter $fs;
42
43 /**
44 * Instance of config repository.
45 *
46 * @var Snippet_Config_Repository
47 */
48 private Snippet_Config_Repository $config_repo;
49
50 /**
51 * Class constructor.
52 *
53 * @param Handler_Registry $handler_registry Registry to use for storing snippet type handlers.
54 * @param Filesystem_Adapter $fs Filesystem adapter to use for writing to files.
55 * @param Snippet_Config_Repository $config_repo Config repository for storing snippets state.
56 */
57 public function __construct(
58 Handler_Registry $handler_registry,
59 Filesystem_Adapter $fs,
60 Snippet_Config_Repository $config_repo
61 ) {
62 $this->handler_registry = $handler_registry;
63 $this->fs = $fs;
64 $this->config_repo = $config_repo;
65 }
66
67 /**
68 * Check if flat files are enabled by checking for the flag file.
69 * This avoids database calls for better performance.
70 *
71 * @return bool True if flat files are enabled, false otherwise.
72 */
73 public static function is_active(): bool {
74 return file_exists( self::get_flag_file_path() );
75 }
76
77 /**
78 * Retrieve the full filesystem path to the flag file, used for determining if flat files are enabled.
79 *
80 * @return string
81 */
82 private static function get_flag_file_path(): string {
83 return self::get_base_dir() . '/' . self::ENABLED_FLAG_FILE;
84 }
85
86 /**
87 * Create or delete the enabled flag file.
88 *
89 * @param bool $enabled Whether file-based execution is enabled.
90 *
91 * @return void
92 */
93 private function handle_enabled_file_flag( bool $enabled ): void {
94 $flag_file_path = self::get_flag_file_path();
95
96 if ( $enabled ) {
97 $base_dir = self::get_base_dir();
98 $this->maybe_create_directory( $base_dir );
99
100 $this->fs->put_contents( $flag_file_path, '', FS_CHMOD_FILE );
101 } else {
102 $this->delete_file( $flag_file_path );
103 }
104 }
105
106 /**
107 * Register WordPress hooks used by file-based execution.
108 *
109 * @return void
110 * @noinspection PhpRedundantOptionalArgumentInspection
111 */
112 public function register_hooks(): void {
113 if ( ! $this->fs->is_writable( WP_CONTENT_DIR ) ) {
114 return;
115 }
116
117 if ( self::is_active() ) {
118 add_action( 'code_snippets/create_snippet', [ $this, 'handle_snippet' ], 10, 2 );
119 add_action( 'code_snippets/update_snippet', [ $this, 'handle_snippet' ], 10, 2 );
120 add_action( 'code_snippets/delete_snippet', [ $this, 'delete_snippet' ], 10, 2 );
121 add_action( 'code_snippets/trash_snippet', [ $this, 'delete_snippet' ], 10, 2 );
122 add_action( 'code_snippets/activate_snippet', [ $this, 'activate_snippet' ], 10, 1 );
123 add_action( 'code_snippets/deactivate_snippet', [ $this, 'deactivate_snippet' ], 10, 2 );
124 add_action( 'code_snippets/activate_snippets', [ $this, 'activate_snippets' ], 10, 2 );
125
126 add_action( 'updated_option', [ $this, 'sync_active_shared_network_snippets' ], 10, 3 );
127 add_action( 'add_option', [ $this, 'sync_active_shared_network_snippets_add' ], 10, 2 );
128 }
129
130 add_filter( 'code_snippets_settings_fields', [ $this, 'add_settings_fields' ], 10, 1 );
131 add_action( 'code_snippets/settings_updated', [ $this, 'create_all_flat_files' ], 10, 1 );
132 }
133
134 /**
135 * Set a number of snippets to active status.
136 *
137 * @param Snippet[] $valid_snippets Snippets to activate.
138 * @param string $table Database table the snippets belong to.
139 *
140 * @return void
141 */
142 public function activate_snippets( array $valid_snippets, string $table ): void {
143 foreach ( $valid_snippets as $snippet ) {
144 $snippet->active = true;
145 $this->handle_snippet( $snippet, $table );
146 }
147 }
148
149 /**
150 * Write a snippet file and update its config index entry.
151 *
152 * @param Snippet $snippet Snippet to write.
153 * @param string $table Snippet database table name.
154 * @param Snippet_Type_Handler $handler Snippet type handler.
155 *
156 * @return void
157 */
158 private function write_snippet( Snippet $snippet, string $table, Snippet_Type_Handler $handler ): void {
159 $hashed_table = self::get_hashed_table_name( $table );
160 $base_dir = self::get_base_dir( $hashed_table, $handler->get_dir_name() );
161 $this->maybe_create_directory( $base_dir );
162
163 $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id, $handler->get_file_extension() );
164
165 $contents = $handler->wrap_code( $snippet->code );
166
167 $this->fs->put_contents( $file_path, $contents, FS_CHMOD_FILE );
168
169 $this->config_repo->update( $base_dir, $snippet );
170 }
171
172 /**
173 * Synchronise a snippet with the filesystem storage.
174 *
175 * @param Snippet $snippet Snippet to synchronise.
176 * @param string $table Database table snippet belongs to.
177 *
178 * @return void
179 */
180 public function handle_snippet( Snippet $snippet, string $table ): void {
181 if ( 0 === $snippet->id ) {
182 return;
183 }
184
185 $handler = $this->handler_registry->get_handler( $snippet->type );
186
187 if ( $handler ) {
188 $this->write_snippet( $snippet, $table, $handler );
189 }
190 }
191
192 /**
193 * Delete a snippet file and remove it from the config index.
194 *
195 * @param Snippet $snippet Snippet to delete.
196 * @param bool $network Whether this is a network-level snippet.
197 *
198 * @return void
199 */
200 public function delete_snippet( Snippet $snippet, bool $network ): void {
201 $handler = $this->handler_registry->get_handler( $snippet->type );
202
203 if ( ! $handler ) {
204 return;
205 }
206
207 $table = self::get_hashed_table_name( code_snippets()->db->get_table_name( $network ) );
208 $base_dir = self::get_base_dir( $table, $handler->get_dir_name() );
209
210 $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id, $handler->get_file_extension() );
211 $this->delete_file( $file_path );
212
213 $this->config_repo->update( $base_dir, $snippet, true );
214 }
215
216 /**
217 * Activate a snippet by writing its code file and updating config.
218 *
219 * @param Snippet $snippet Snippet object.
220 *
221 * @return void
222 */
223 public function activate_snippet( Snippet $snippet ): void {
224 $snippet = get_snippet( $snippet->id, $snippet->network );
225 $handler = $this->handler_registry->get_handler( $snippet->type );
226
227 if ( $handler ) {
228 $table = code_snippets()->db->get_table_name( $snippet->network );
229 $this->write_snippet( $snippet, $table, $handler );
230 }
231 }
232
233 /**
234 * Deactivate a snippet by updating its config entry.
235 *
236 * @param int $snippet_id Snippet ID.
237 * @param bool $network Whether the snippet is network-wide.
238 *
239 * @return void
240 */
241 public function deactivate_snippet( int $snippet_id, bool $network ): void {
242 $snippet = get_snippet( $snippet_id, $network );
243 $handler = $this->handler_registry->get_handler( $snippet->type );
244
245 if ( ! $handler ) {
246 return;
247 }
248
249 $table = self::get_hashed_table_name( code_snippets()->db->get_table_name( $network ) );
250 $base_dir = self::get_base_dir( $table, $handler->get_dir_name() );
251
252 $this->config_repo->update( $base_dir, $snippet );
253 }
254
255 /**
256 * Determine the base directory for storing a snippet given its database table and type.
257 *
258 * @param string $table Database table name (can be empty).
259 * @param string $snippet_type Snippet type (can be empty).
260 *
261 * @return string Full filesystem path to base directory.
262 */
263 public static function get_base_dir( string $table = '', string $snippet_type = '' ): string {
264 $base_dir = WP_CONTENT_DIR . '/code-snippets';
265
266 if ( ! empty( $table ) ) {
267 $base_dir .= '/' . $table;
268 }
269
270 if ( ! empty( $snippet_type ) ) {
271 $base_dir .= '/' . $snippet_type;
272 }
273
274 return $base_dir;
275 }
276
277 /**
278 * Get the base URL for flat files.
279 *
280 * @param string $table Optional hashed table name.
281 * @param string $snippet_type Optional snippet type directory.
282 *
283 * @return string
284 */
285 public static function get_base_url( string $table = '', string $snippet_type = '' ): string {
286 $base_url = WP_CONTENT_URL . '/code-snippets';
287
288 if ( ! empty( $table ) ) {
289 $base_url .= '/' . $table;
290 }
291
292 if ( ! empty( $snippet_type ) ) {
293 $base_url .= '/' . $snippet_type;
294 }
295
296 return $base_url;
297 }
298
299 /**
300 * Create a new directory if it does not already exist.
301 *
302 * @param string $dir Directory path.
303 *
304 * @return void
305 */
306 private function maybe_create_directory( string $dir ): void {
307 if ( ! $this->fs->is_dir( $dir ) ) {
308 $result = wp_mkdir_p( $dir );
309
310 if ( $result ) {
311 $this->fs->chmod( $dir, FS_CHMOD_DIR );
312 }
313 }
314 }
315
316 /**
317 * Determine the file path for a snippet.
318 *
319 * @param string $base_dir Base filesystem directory.
320 * @param int $snippet_id Snippet identifier.
321 * @param string $ext File extension, without the period.
322 *
323 * @return string
324 */
325 private function get_snippet_file_path( string $base_dir, int $snippet_id, string $ext ): string {
326 return trailingslashit( $base_dir ) . $snippet_id . '.' . $ext;
327 }
328
329 /**
330 * Delete a file from the filesystem if it exists.
331 *
332 * @param string $file_path Path of file to delete.
333 *
334 * @return void
335 */
336 private function delete_file( string $file_path ): void {
337 if ( $this->fs->exists( $file_path ) ) {
338 $this->fs->delete( $file_path );
339 }
340 }
341
342 /**
343 * Sync the active shared network snippets list to a config file.
344 *
345 * @param string $option Option name.
346 * @param mixed $old_value Previous value.
347 * @param mixed $value New value.
348 *
349 * @return void
350 * @noinspection PhpUnusedParameterInspection
351 */
352 public function sync_active_shared_network_snippets( string $option, $old_value, $value ): void {
353 if ( 'active_shared_network_snippets' !== $option ) {
354 return;
355 }
356
357 $this->create_active_shared_network_snippets_file( $value );
358 }
359
360 /**
361 * Handler for 'add_option' to ensure that the stored active network snippet statuses match that in the database.
362 *
363 * @param string|mixed $option Name of option being added.
364 * @param mixed $value Initial value of option.
365 *
366 * @return void
367 */
368 public function sync_active_shared_network_snippets_add( $option, $value ): void {
369 if ( 'active_shared_network_snippets' !== $option ) {
370 return;
371 }
372
373 $this->create_active_shared_network_snippets_file( $value );
374 }
375
376 /**
377 * Create or update the active shared network snippets config file.
378 *
379 * @param mixed $value Option value.
380 *
381 * @return void
382 *
383 * phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_var_export
384 */
385 private function create_active_shared_network_snippets_file( $value ): void {
386 $table = self::get_hashed_table_name( code_snippets()->db->get_table_name( false ) );
387 $base_dir = self::get_base_dir( $table );
388
389 $this->maybe_create_directory( $base_dir );
390 $file_path = trailingslashit( $base_dir ) . 'active-shared-network-snippets.php';
391
392 $file_content = sprintf(
393 "<?php\n\nif ( ! defined( 'ABSPATH' ) ) { return; }\n\nreturn %s;\n",
394 var_export( $value, true )
395 );
396
397 $this->fs->put_contents( $file_path, $file_content, FS_CHMOD_FILE );
398 }
399
400 /**
401 * Hash a table name.
402 *
403 * @param string $table Table name to hash.
404 *
405 * @return string Hashed table name.
406 */
407 public static function get_hashed_table_name( string $table ): string {
408 // wp_hash() is pluggable and may not be available during early bootstrap.
409 return function_exists( 'wp_hash' ) ? wp_hash( $table ) : md5( $table );
410 }
411
412 /**
413 * Get a list of active snippets from flat file config.
414 *
415 * @param array<string> $scopes Scopes to include.
416 * @param string $snippet_type Snippet type directory.
417 *
418 * @return array<int, array<string, mixed>>
419 */
420 public static function get_active_snippets_from_flat_files(
421 array $scopes = [],
422 string $snippet_type = 'php'
423 ): array {
424 $active_snippets = [];
425 $db = code_snippets()->db;
426
427 // Always use the site table for "local" snippets, even in Network Admin.
428 $table = self::get_hashed_table_name( $db->get_table_name( false ) );
429 $snippets = self::load_active_snippets_from_file(
430 $table,
431 $snippet_type,
432 $scopes
433 );
434
435 if ( $snippets ) {
436 foreach ( $snippets as $snippet ) {
437 $active_snippets[] = [
438 'id' => intval( $snippet['id'] ),
439 'code' => $snippet['code'],
440 'scope' => $snippet['scope'],
441 'table' => $db->table,
442 'network' => false,
443 'priority' => intval( $snippet['priority'] ),
444 'condition_id' => intval( $snippet['condition_id'] ),
445 ];
446 }
447 }
448
449 if ( is_multisite() ) {
450 $ms_table = self::get_hashed_table_name( $db->get_table_name( true ) );
451
452 $root_base_dir = self::get_base_dir( $table );
453 $active_shared_ids_file_path = $root_base_dir . '/active-shared-network-snippets.php';
454 $active_shared_ids = is_file( $active_shared_ids_file_path )
455 ? require $active_shared_ids_file_path
456 : [];
457
458 $ms_snippets = self::load_active_snippets_from_file(
459 $ms_table,
460 $snippet_type,
461 $scopes,
462 $active_shared_ids
463 );
464
465 if ( $ms_snippets ) {
466 $active_shared_ids = is_array( $active_shared_ids )
467 ? array_map( 'intval', $active_shared_ids )
468 : [];
469
470 foreach ( $ms_snippets as $snippet ) {
471 $id = intval( $snippet['id'] );
472 $active_value = intval( $snippet['active'] );
473
474 if ( ! DB::is_network_snippet_enabled( $active_value, $id, $active_shared_ids ) ) {
475 continue;
476 }
477
478 $active_snippets[] = [
479 'id' => $id,
480 'code' => $snippet['code'],
481 'scope' => $snippet['scope'],
482 'table' => $db->ms_table,
483 'network' => true,
484 'priority' => intval( $snippet['priority'] ),
485 'condition_id' => intval( $snippet['condition_id'] ),
486 ];
487 }
488
489 self::sort_active_snippets( $active_snippets, $db );
490 }
491 }
492
493 return $active_snippets;
494 }
495
496 /**
497 * Sort list of active snippets for evaluation.
498 *
499 * @param array $active_snippets List of active snippet data.
500 * @param DB $db Database instance.
501 *
502 * @return void
503 */
504 private static function sort_active_snippets( array &$active_snippets, DB $db ): void {
505 $comparisons = [
506 function ( array $a, array $b ) {
507 return $a['priority'] <=> $b['priority'];
508 },
509 function ( array $a, array $b ) use ( $db ) {
510 $a_table = $a['table'] === $db->ms_table ? 0 : 1;
511 $b_table = $b['table'] === $db->ms_table ? 0 : 1;
512 return $a_table <=> $b_table;
513 },
514 function ( array $a, array $b ) {
515 return $a['id'] <=> $b['id'];
516 },
517 ];
518
519 usort(
520 $active_snippets,
521 static function ( $a, $b ) use ( $comparisons ) {
522 foreach ( $comparisons as $comparison ) {
523 $result = $comparison( $a, $b );
524 if ( 0 !== $result ) {
525 return $result;
526 }
527 }
528
529 return 0;
530 }
531 );
532 }
533
534 /**
535 * Load active snippets from a flat file config index.
536 *
537 * @param string $table Hashed table directory name.
538 * @param string $snippet_type Snippet type directory.
539 * @param string[] $scopes Scopes to include.
540 * @param int[]|null $active_shared_ids Optional list of active shared network snippet IDs.
541 *
542 * @return array<int, array<string, mixed>>
543 */
544 private static function load_active_snippets_from_file(
545 string $table,
546 string $snippet_type,
547 array $scopes,
548 ?array $active_shared_ids = null
549 ): array {
550 $snippets = [];
551 $db = code_snippets()->db;
552
553 $base_dir = self::get_base_dir( $table, $snippet_type );
554 $snippets_file_path = $base_dir . '/index.php';
555
556 if ( ! is_file( $snippets_file_path ) ) {
557 return $snippets;
558 }
559
560 $cache_key = sprintf(
561 'active_snippets_%s_%s',
562 sanitize_key( join( '_', $scopes ) ),
563 self::get_hashed_table_name( $db->table ) === $table ? $db->table : $db->ms_table
564 );
565
566 $cached_snippets = wp_cache_get( $cache_key, CACHE_GROUP );
567
568 if ( is_array( $cached_snippets ) ) {
569 return $cached_snippets;
570 }
571
572 $file_snippets = require $snippets_file_path;
573 $shared_ids = is_array( $active_shared_ids )
574 ? array_map( 'intval', $active_shared_ids )
575 : [];
576
577 $filtered_snippets = array_filter(
578 $file_snippets,
579 function ( $snippet ) use ( $scopes, $shared_ids ) {
580 $active_value = isset( $snippet['active'] ) ? intval( $snippet['active'] ) : 0;
581
582 $is_active = DB::is_network_snippet_enabled( $active_value, intval( $snippet['id'] ), $shared_ids );
583
584 return ( $is_active || 'condition' === $snippet['scope'] ) &&
585 in_array( $snippet['scope'], $scopes, true );
586 }
587 );
588
589 wp_cache_set( $cache_key, $filtered_snippets, CACHE_GROUP );
590
591 return $filtered_snippets;
592 }
593
594 /**
595 * Add file-based execution settings fields.
596 *
597 * @param array<string, mixed> $fields Settings fields.
598 *
599 * @return array<string, mixed> Settings fields with flat file setting added.
600 */
601 public function add_settings_fields( array $fields ): array {
602
603 $learn_more_link = sprintf(
604 ' <a href="%s" target="_blank" rel="noopener noreferrer">%s</a>',
605 esc_url( 'https://codesnippets.pro/doc/file-based-execution/' ),
606 __( 'Learn more.', 'code-snippets' )
607 );
608
609 $fields['general']['enable_flat_files'] = [
610 'name' => __( 'Enable File-Based Execution', 'code-snippets' ),
611 'type' => 'checkbox',
612 'label' => __( 'Snippets will be executed directly from files instead of the database.', 'code-snippets' ) . $learn_more_link,
613 ];
614
615 return $fields;
616 }
617
618 /**
619 * Create necessary flat files, if the option is enabled.
620 *
621 * @param array<string, mixed> $settings Settings data.
622 *
623 * @return void
624 */
625 public function create_all_flat_files( array $settings ): void {
626 if ( ! isset( $settings['general']['enable_flat_files'] ) ) {
627 return;
628 }
629
630 $this->handle_enabled_file_flag( $settings['general']['enable_flat_files'] );
631
632 if ( ! $settings['general']['enable_flat_files'] ) {
633 return;
634 }
635
636 $this->create_snippet_flat_files();
637 $this->create_active_shared_network_snippets_config_file();
638 }
639
640 /**
641 * Create snippet code files and config indexes for all active snippets.
642 *
643 * @return void
644 */
645 private function create_snippet_flat_files(): void {
646 $db = code_snippets()->db;
647
648 $scopes = Snippet::get_all_scopes();
649
650 $data = $db->fetch_active_snippets( $scopes );
651
652 foreach ( $data as $snippet ) {
653 $snippet_obj = get_snippet( $snippet['id'], $db->ms_table === $snippet['table'] );
654 $this->handle_snippet( $snippet_obj, $snippet['table'] );
655 }
656
657 if ( is_multisite() ) {
658 $sites = get_sites( [ 'fields' => 'ids' ] );
659 foreach ( $sites as $site_id ) {
660 switch_to_blog( $site_id );
661 $db->set_table_vars();
662
663 $site_data = $db->fetch_active_snippets( $scopes );
664 foreach ( $site_data as $snippet ) {
665 $table_name = $snippet['table'];
666 $snippet_obj = get_snippet( $snippet['id'], false );
667 $this->handle_snippet( $snippet_obj, $table_name );
668 }
669
670 restore_current_blog();
671 }
672
673 $db->set_table_vars();
674 }
675 }
676
677 /**
678 * Create active shared network snippet config files for each site (multisite) or the current site.
679 *
680 * @return void
681 */
682 private function create_active_shared_network_snippets_config_file(): void {
683 if ( is_multisite() ) {
684 $db = code_snippets()->db;
685 $sites = get_sites( [ 'fields' => 'ids' ] );
686
687 foreach ( $sites as $site_id ) {
688 switch_to_blog( $site_id );
689 $db->set_table_vars();
690
691 $active_shared_network_snippets = get_option( 'active_shared_network_snippets' );
692 if ( false !== $active_shared_network_snippets ) {
693 $this->create_active_shared_network_snippets_file( $active_shared_network_snippets );
694 }
695
696 restore_current_blog();
697 }
698
699 $db->set_table_vars();
700 } else {
701 $active_shared_network_snippets = get_option( 'active_shared_network_snippets' );
702 if ( false !== $active_shared_network_snippets ) {
703 $this->create_active_shared_network_snippets_file( $active_shared_network_snippets );
704 }
705 }
706 }
707 }
708