PluginProbe
Code Snippets / 4.0.0-beta.2
Code Snippets v4.0.0-beta.2
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 / Flat_Files / Snippet_Files.php

Snippet_Files.php in Code Snippets 4.0.0-beta.2, at php/Flat_Files/Snippet_Files.php

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