PluginProbe
Code Snippets / 3.9.6
Code Snippets v3.9.6
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 / classes / class-snippet-files.php

class-snippet-files.php in Code Snippets 3.9.6, at php/flat-files/classes/class-snippet-files.php

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