PluginProbe
TablePress – Tables in WordPress made easy / 2.3
TablePress – Tables in WordPress made easy v2.3
3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 2.4.4 All 44 releases
tablepress / models / model-table.php

model-table.php in TablePress – Tables in WordPress made easy 2.3, at models/model-table.php

1,234 lines 41.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Table Model
4 *
5 * @package TablePress
6 * @subpackage Models
7 * @author Tobias Bäthge
8 * @since 1.0.0
9 */
10
11 // Prohibit direct script loading.
12 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
13
14 /**
15 * Table Model class
16 *
17 * @package TablePress
18 * @subpackage Models
19 * @author Tobias Bäthge
20 * @since 1.0.0
21 */
22 class TablePress_Table_Model extends TablePress_Model {
23
24 /**
25 * Instance of the Post Type Model.
26 *
27 * @since 1.0.0
28 * @var TablePress_Post_Model
29 */
30 protected $model_post;
31
32 /**
33 * Name of the Post Meta Field for table options.
34 *
35 * @since 1.0.0
36 * @var string
37 */
38 protected $table_options_field_name = '_tablepress_table_options';
39
40 /**
41 * Name of the Post Meta Field for table visibility.
42 *
43 * @since 1.0.0
44 * @var string
45 */
46 protected $table_visibility_field_name = '_tablepress_table_visibility';
47
48 /**
49 * Default set of tables.
50 *
51 * @since 1.0.0
52 * @var array{last_id: int, table_post: array<string, int>} {
53 * @type int $last_id Last table ID that was given to a new table.
54 * @type array<string, int> $table_post Connections between table ID and post ID (key: table ID, value: post ID).
55 * }
56 */
57 protected $default_tables = array(
58 'last_id' => 0,
59 'table_post' => array(),
60 );
61
62 /**
63 * Instance of WP_Option class for the list of tables.
64 *
65 * @since 1.0.0
66 * @var TablePress_WP_Option
67 */
68 protected $tables;
69
70 /**
71 * Init the Table model by instantiating a Post model and loading the list of tables option.
72 *
73 * @since 1.0.0
74 */
75 public function __construct() {
76 parent::__construct();
77 $this->model_post = TablePress::load_model( 'post' );
78
79 $params = array(
80 'option_name' => 'tablepress_tables',
81 'default_value' => $this->default_tables,
82 );
83 $this->tables = TablePress::load_class( 'TablePress_WP_Option', 'class-wp_option.php', 'classes', $params );
84 }
85
86 /**
87 * Get the tables option, which holds the connection between table ID and post ID.
88 *
89 * @since 1.0.0
90 *
91 * @return mixed[] Current set of tables.
92 */
93 public function _debug_get_tables(): array {
94 return $this->tables->get();
95 }
96
97 /**
98 * Update the tables option, which holds the connection between table ID and post ID.
99 *
100 * @since 1.0.0
101 *
102 * @param mixed[] $tables New set of tables.
103 */
104 public function _debug_update_tables( array $tables ): void {
105 $this->tables->update( $tables );
106 }
107
108 /**
109 * Convert a table to a post, which can be stored in the database.
110 *
111 * @since 1.0.0
112 *
113 * @param array<string, mixed> $table Table.
114 * @param int $post_id Post ID of an existing table, or -1 for a new table.
115 * @return array<string, mixed> Post.
116 */
117 protected function _table_to_post( array $table, int $post_id ): array {
118 // Run filters on content in each cell and other fields.
119 $table = $this->filter_content( $table );
120
121 // Sanitize each cell, table name, and table description, if the user is not allowed to work with unfiltered HTML.
122 if ( ! current_user_can( 'unfiltered_html' ) ) {
123 $table = $this->sanitize( $table );
124 }
125
126 // New posts have a post ID of false in WordPress.
127 if ( -1 === $post_id ) {
128 $post_id = false;
129 }
130
131 $post = array(
132 'ID' => $post_id,
133 'post_title' => $table['name'],
134 'post_excerpt' => $table['description'],
135 'post_content' => wp_json_encode( $table['data'], TABLEPRESS_JSON_OPTIONS ),
136 'post_mime_type' => 'application/json',
137 );
138
139 return $post;
140 }
141
142 /**
143 * Convert a post (from the database) to a table.
144 *
145 * @since 1.0.0
146 *
147 * @param WP_Post $post Post.
148 * @param string $table_id Table ID.
149 * @param bool $load_data Whether the table data shall be loaded.
150 * @return array<string, mixed> Table.
151 */
152 protected function _post_to_table( WP_Post $post, string $table_id, bool $load_data ): array {
153 $table = array(
154 'id' => $table_id,
155 'name' => $post->post_title,
156 'description' => $post->post_excerpt,
157 'author' => $post->post_author,
158 // 'created' => $post->post_date,
159 'last_modified' => $post->post_modified,
160 );
161
162 if ( ! $load_data ) {
163 return $table;
164 }
165
166 $table['data'] = json_decode( $post->post_content, true );
167
168 // Check if JSON could be decoded.
169 if ( is_null( $table['data'] ) ) {
170 $table['data'] = array( array( "The internal data of table {$table_id} is corrupted." ) ); // Set a single cell as the cell content.
171 $table['is_corrupted'] = true;
172 $table['json_error'] = json_last_error_msg();
173 $table['description'] = "[ERROR] TABLE IS CORRUPTED (JSON error: {$table['json_error']})! DO NOT EDIT THIS TABLE NOW!\nInstead, please see https://tablepress.org/faq/corrupted-tables/ for instructions.\n-\n{$table['description']}";
174 } else {
175 // Specifically cast to an array again.
176 $table['data'] = (array) $table['data'];
177 }
178
179 return $table;
180 }
181
182 /**
183 * Load a table.
184 *
185 * @since 1.0.0
186 *
187 * @param string $table_id Table ID.
188 * @param bool $load_data Whether the table data shall be loaded.
189 * @param bool $load_options_visibility Whether the table options and table visibility shall be loaded.
190 * @return array<string, mixed>|WP_Error Table as an array on success, WP_Error on error.
191 */
192 public function load( string $table_id, bool $load_data = true, bool $load_options_visibility = true ) /* : array|WP_Error */ {
193 if ( empty( $table_id ) ) {
194 return new WP_Error( 'table_load_empty_table_id' );
195 }
196
197 $post_id = $this->_get_post_id( $table_id );
198 if ( false === $post_id ) {
199 return new WP_Error( 'table_load_no_post_id_for_table_id', '', $table_id );
200 }
201
202 $post = $this->model_post->get( $post_id );
203 if ( false === $post ) {
204 return new WP_Error( 'table_load_no_post_for_post_id', '', $post_id );
205 }
206
207 $table = $this->_post_to_table( $post, $table_id, $load_data );
208 if ( $load_options_visibility ) {
209 $table['options'] = $this->_get_table_options( $post_id );
210 $table['visibility'] = $this->_get_table_visibility( $post_id );
211 }
212 return $table;
213 }
214
215 /**
216 * Load the IDs of all tables that can be loaded from the database.
217 *
218 * @since 1.0.0
219 *
220 * @param bool $prime_meta_cache Optional. Whether the prime the post meta cache when loading the posts.
221 * @param bool $run_filter Optional. Whether to run a filter on the list of table IDs.
222 * @return string[] Array of table IDs.
223 */
224 public function load_all( bool $prime_meta_cache = true, bool $run_filter = true ): array {
225 $table_post = $this->tables->get( 'table_post' );
226 if ( empty( $table_post ) ) {
227 return array();
228 }
229
230 // Load all table posts with one query, to prime the cache.
231 $this->model_post->load_posts( array_values( $table_post ), $prime_meta_cache );
232
233 // This loop now uses the WP cache.
234 $table_ids = array();
235 foreach ( $table_post as $table_id => $post_id ) {
236 $table_id = (string) $table_id; // Ensure that the table ID is a string, as it comes from an array key where numeric strings are converted to integers.
237
238 // Load table without data and options to save memory.
239 $table = $this->load( $table_id, false, false );
240
241 // Skip tables that could not be loaded properly.
242 if ( ! is_wp_error( $table ) ) {
243 $table_ids[] = $table_id;
244 }
245 }
246
247 if ( $run_filter ) {
248 /**
249 * Filters all table IDs that are loaded.
250 *
251 * @since 1.4.0
252 *
253 * @param string[] $table_ids The table IDs that are loaded.
254 */
255 $table_ids = apply_filters( 'tablepress_load_all_tables', $table_ids );
256 }
257
258 return $table_ids;
259 }
260
261 /**
262 * Sanitize the table to remove undesired HTML code using KSES.
263 *
264 * @since 1.8.0
265 *
266 * @param array<string, mixed> $table Table.
267 * @return array<string, mixed> Sanitized table.
268 */
269 public function sanitize( array $table ): array {
270 // Sanitize the table name and description.
271 $fields = array( 'name', 'description' );
272 foreach ( $fields as $field ) {
273 $table[ $field ] = wp_kses_post( $table[ $field ] );
274 }
275
276 // Sanitize each cell.
277 foreach ( $table['data'] as $row_idx => $row ) {
278 foreach ( $row as $column_idx => $cell_content ) {
279 $table['data'][ $row_idx ][ $column_idx ] = wp_kses_post( $cell_content ); // Equals wp_filter_post_kses(), but without the unnecessary slashes handling.
280 }
281 }
282
283 return $table;
284 }
285
286 /**
287 * Filter/modify the content of table cells and other fields, e.g. for security hardening.
288 *
289 * This is similar to the `sanitize()` method, but executed for all users.
290 * In 1.10.0, adding `rel="noopener noreferrer"` to all HTML link elements like `<a target=` was added. See https://core.trac.wordpress.org/ticket/43187.
291 * Since 1.13.0, and on WP 5.6, only `rel="noopener"` is added. See https://core.trac.wordpress.org/ticket/49558.
292 *
293 * @since 1.10.0
294 *
295 * @param array<string, mixed> $table Table.
296 * @return array<string, mixed> Filtered/modified table.
297 */
298 public function filter_content( array $table ): array {
299 /**
300 * Filters whether the contents of table cells and fields should be filtered/modified.
301 *
302 * @since 1.10.0
303 *
304 * @param bool $filter Whether to filter the content of table cells and other fields. Default true.
305 */
306 if ( ! apply_filters( 'tablepress_filter_table_cell_content', true ) ) {
307 return $table;
308 }
309
310 // Filter the table name and description.
311 $fields = array( 'name', 'description' );
312 foreach ( $fields as $field ) {
313 $table[ $field ] = wp_targeted_link_rel( $table[ $field ] );
314 }
315
316 foreach ( $table['data'] as $row_idx => $row ) {
317 foreach ( $row as $column_idx => $cell_content ) {
318 $table['data'][ $row_idx ][ $column_idx ] = wp_targeted_link_rel( $cell_content );
319 }
320 }
321
322 return $table;
323 }
324
325 /**
326 * Save a table.
327 *
328 * @since 1.0.0
329 *
330 * @param array<string, mixed> $table Table (needs to have $table['id']!).
331 * @return string|WP_Error WP_Error on error, string table ID on success.
332 */
333 public function save( array $table ) /* : string|WP_Error */ {
334 if ( empty( $table['id'] ) ) {
335 return new WP_Error( 'table_save_empty_table_id' );
336 }
337
338 $post_id = $this->_get_post_id( $table['id'] );
339 if ( false === $post_id ) {
340 return new WP_Error( 'table_save_no_post_id_for_table_id', '', $table['id'] );
341 }
342
343 $post = $this->_table_to_post( $table, $post_id );
344 $new_post_id = $this->model_post->update( $post );
345 if ( is_wp_error( $new_post_id ) ) {
346 $error = new WP_Error( 'table_save_post_update', '', $post_id );
347 $error->merge_from( $new_post_id );
348 return $error;
349 }
350 if ( $post_id !== $new_post_id ) {
351 return new WP_Error( 'table_save_new_post_id_does_not_match', '', $new_post_id );
352 }
353
354 $options_saved = $this->_update_table_options( $new_post_id, $table['options'] );
355 if ( ! $options_saved ) {
356 return new WP_Error( 'table_save_update_table_options_failed', '', $new_post_id );
357 }
358
359 $visibility_saved = $this->_update_table_visibility( $new_post_id, $table['visibility'] );
360 if ( ! $visibility_saved ) {
361 return new WP_Error( 'table_save_update_table_visibility_failed', '', $new_post_id );
362 }
363
364 // At this point, post was successfully added.
365
366 // Invalidate table output caches that belong to this table.
367 $this->invalidate_table_output_cache( $table['id'] );
368 // Flush caching plugins' caches.
369 $this->_flush_caching_plugins_caches();
370
371 /**
372 * Fires after a table has been saved.
373 *
374 * @since 1.5.0
375 *
376 * @param string $table_id ID of the added table.
377 */
378 do_action( 'tablepress_event_saved_table', $table['id'] );
379
380 return $table['id'];
381 }
382
383 /**
384 * Add a new table.
385 *
386 * @since 1.0.0
387 *
388 * @param array<string, mixed> $table Table ($table['id'] is not necessary).
389 * @param string $copy_or_add Optional. 'copy' if the table is copied, 'add' if it is a new table. Default 'add'.
390 * @return string|WP_Error WP_Error on error, string table ID of the new table on success.
391 */
392 public function add( array $table, string $copy_or_add = 'add' ) /* : string|WP_Error */ {
393 $post_id = -1; // To insert table.
394 $post = $this->_table_to_post( $table, $post_id );
395 $new_post_id = $this->model_post->insert( $post );
396 if ( is_wp_error( $new_post_id ) ) {
397 $error = new WP_Error( 'table_add_post_insert', '' );
398 $error->merge_from( $new_post_id );
399 return $error;
400 }
401
402 $options_saved = $this->_add_table_options( $new_post_id, $table['options'] );
403 if ( ! $options_saved ) {
404 return new WP_Error( 'table_add_update_table_options_failed', '', $new_post_id );
405 }
406
407 $visibility_saved = $this->_add_table_visibility( $new_post_id, $table['visibility'] );
408 if ( ! $visibility_saved ) {
409 return new WP_Error( 'table_add_update_table_visibility_failed', '', $new_post_id );
410 }
411
412 // At this point, post was successfully added, now get an unused table ID.
413 $table_id = $this->_get_new_table_id();
414 $this->_update_post_id( $table_id, $new_post_id );
415
416 if ( 'add' === $copy_or_add ) {
417 /**
418 * Fires after a new table has been added.
419 *
420 * @since 1.1.0
421 *
422 * @param string $table_id ID of the added table.
423 */
424 do_action( 'tablepress_event_added_table', $table_id );
425 }
426
427 return $table_id;
428 }
429
430 /**
431 * Create a copy of a table and add it.
432 *
433 * @since 1.0.0
434 *
435 * @param string $table_id ID of the table to be copied.
436 * @return string|WP_Error WP_Error on error, string table ID of the new table on success.
437 */
438 public function copy( string $table_id ) /* : string|WP_Error */ {
439 $table = $this->load( $table_id, true, true );
440 if ( is_wp_error( $table ) ) {
441 $error = new WP_Error( 'table_copy_table_load', '', $table_id );
442 $error->merge_from( $table );
443 return $error;
444 }
445
446 // Adjust name of copied table.
447 if ( '' === trim( $table['name'] ) ) {
448 $table['name'] = __( '(no name)', 'tablepress' );
449 }
450 $table['name'] = sprintf( __( 'Copy of %s', 'tablepress' ), $table['name'] );
451
452 // Merge this data into an empty table template.
453 $table = $this->prepare_table( $this->get_table_template(), $table, false );
454 if ( is_wp_error( $table ) ) {
455 $error = new WP_Error( 'table_copy_table_prepare', '', $table_id );
456 $error->merge_from( $table );
457 return $error;
458 }
459
460 // Add the copied table.
461 $new_table_id = $this->add( $table, 'copy' );
462 if ( is_wp_error( $new_table_id ) ) {
463 $error = new WP_Error( 'table_copy_table_add', '', $table_id );
464 $error->merge_from( $new_table_id );
465 return $error;
466 }
467
468 /**
469 * Fires after an existing table has been copied.
470 *
471 * @since 1.1.0
472 *
473 * @param string $new_table_id ID of the copy of the table.
474 * @param string $table_id ID of the existing table that is copied.
475 */
476 do_action( 'tablepress_event_copied_table', $new_table_id, $table_id );
477
478 return $new_table_id;
479 }
480
481 /**
482 * Delete a table (and its options).
483 *
484 * @since 1.0.0
485 *
486 * @param string $table_id ID of the table to be deleted.
487 * @return bool|WP_Error WP_Error on error, true on success.
488 */
489 public function delete( string $table_id ) /* : true|WP_Error */ {
490 if ( ! $this->table_exists( $table_id ) ) {
491 return new WP_Error( 'table_delete_table_does_not_exist', '', $table_id );
492 }
493
494 $post_id = $this->_get_post_id( $table_id ); // No ! false check necessary, as this is covered by table_exists() check above.
495 $deleted = $this->model_post->delete( $post_id ); // Post Meta fields will be deleted automatically by that function. // @phpstan-ignore-line .
496 if ( false === $deleted ) {
497 return new WP_Error( 'table_delete_post_could_not_be_deleted', '', $post_id );
498 }
499
500 // If post was deleted successfully, remove the table ID from the list of tables.
501 $this->_remove_post_id( $table_id );
502
503 // Invalidate table output caches that belong to this table.
504 $this->invalidate_table_output_cache( $table_id );
505 // Flush caching plugins' caches.
506 $this->_flush_caching_plugins_caches();
507
508 /**
509 * Fires after a table has been deleted.
510 *
511 * @since 1.1.0
512 *
513 * @param string $table_id ID of the deleted table.
514 */
515 do_action( 'tablepress_event_deleted_table', $table_id );
516
517 return true;
518 }
519
520 /**
521 * Delete all tables.
522 *
523 * @since 1.0.0
524 */
525 public function delete_all(): void {
526 $tables = $this->tables->get();
527 if ( empty( $tables['table_post'] ) ) {
528 return;
529 }
530
531 foreach ( $tables['table_post'] as $table_id => $post_id ) {
532 $table_id = (string) $table_id; // Ensure that the table ID is a string, as it comes from an array key where numeric strings are converted to integers.
533
534 $this->model_post->delete( $post_id ); // Post Meta fields will be deleted automatically by that function.
535 unset( $tables['table_post'][ $table_id ] );
536
537 // Invalidate table output caches that belong to this table.
538 $this->invalidate_table_output_cache( $table_id );
539 }
540
541 $this->tables->update( $tables );
542 // Flush caching plugins' caches.
543 $this->_flush_caching_plugins_caches();
544
545 /**
546 * Fires after all tables have been deleted.
547 *
548 * @since 1.1.0
549 */
550 do_action( 'tablepress_event_deleted_all_tables' );
551 }
552
553 /**
554 * Check if a table ID exists in the list of tables (this does not guarantee that the post with the table data exists!).
555 *
556 * @since 1.0.0
557 *
558 * @param string $table_id Table ID.
559 * @return bool Whether the table ID exists.
560 */
561 public function table_exists( string $table_id ): bool {
562 $table_post = $this->tables->get( 'table_post' );
563 return isset( $table_post[ $table_id ] );
564 }
565
566 /**
567 * Count the number of tables from either just the list, or by also counting the posts in the database.
568 *
569 * @since 1.0.0
570 *
571 * @param bool $single_value Optional. Whether to return just the number of tables from the list, or also count in the database.
572 * @return int|array{list: int, db: int} Number of Tables (if $single_value), or array of Numbers from list/DB (if ! $single_value).
573 */
574 public function count_tables( bool $single_value = true ) /* : int|array */ {
575 $count_list = count( $this->tables->get( 'table_post' ) );
576 if ( $single_value ) {
577 return $count_list;
578 }
579
580 $count_db = $this->model_post->count_posts();
581 return array(
582 'list' => $count_list,
583 'db' => $count_db,
584 );
585 }
586
587 /**
588 * Delete all transients used for output caching of a table (e.g. when the table is updated or deleted).
589 *
590 * @since 1.0.0
591 * @since 1.8.0 Renamed from _invalidate_table_output_cache to invalidate_table_output_cache and made public.
592 *
593 * @param string $table_id Table ID.
594 */
595 public function invalidate_table_output_cache( string $table_id ): void {
596 $caches_list_transient_name = 'tablepress_c_' . md5( $table_id );
597 $caches_list = get_transient( $caches_list_transient_name );
598 if ( false !== $caches_list ) {
599 $caches_list = (array) json_decode( $caches_list, true );
600 foreach ( $caches_list as $cache_transient_name ) {
601 delete_transient( $cache_transient_name );
602 }
603 }
604 delete_transient( $caches_list_transient_name );
605 }
606
607 /**
608 * Flush the caches of common caching plugins.
609 *
610 * @since 1.0.0
611 */
612 public function _flush_caching_plugins_caches(): void {
613 /**
614 * Filters whether the caches of common caching plugins shall be flushed.
615 *
616 * @since 1.0.0
617 *
618 * @param bool $flush Whether caches of caching plugins shall be flushed. Default true.
619 */
620 if ( ! apply_filters( 'tablepress_flush_caching_plugins_caches', true ) ) {
621 return;
622 }
623
624 // Common cache flush callback.
625 $cache_flush_callbacks = array(
626 array( 'Breeze_PurgeCache', 'breeze_cache_flush' ), // Breeze.
627 array( 'comet_cache', 'clear' ), // Comet Cache.
628 'pantheon_wp_clear_edge_all', // Pantheon.
629 'sg_cachepress_purge_cache', // SG Optimizer.
630 array( 'Swift_Performance_Cache', 'clear_all_cache' ), // Swift Performance.
631 'w3tc_pgcache_flush', // W3 Total Cache.
632 array( 'WpeCommon', 'purge_memcached' ), // WP Engine.
633 array( 'WpeCommon', 'clear_maxcdn_cache' ), // WP Engine.
634 array( 'WpeCommon', 'purge_varnish_cache' ), // WP Engine.
635 'wpfc_clear_all_cache', // WP Fastest Cache.
636 'rocket_clean_domain', // WP Rocket.
637 'wp_cache_clear_cache', // WP Super Cache.
638 array( 'zencache', 'clear' ), // Zen Cache.
639 );
640 foreach ( $cache_flush_callbacks as $cache_flush_callback ) {
641 if ( is_callable( $cache_flush_callback ) ) {
642 call_user_func( $cache_flush_callback );
643 }
644 }
645
646 // Common cache flush hooks.
647 $cache_flush_hooks = array(
648 'ce_clear_cache', // Cache Enabler.
649 'cachify_flush_cache', // Cachify.
650 'autoptimize_action_cachepurged', // Hyper Cache.
651 );
652 foreach ( $cache_flush_hooks as $cache_flush_hook ) {
653 do_action( $cache_flush_hook );
654 }
655
656 // Kinsta.
657 if ( isset( $GLOBALS['kinsta_cache'] ) && ! empty( $GLOBALS['kinsta_cache']->kinsta_cache_purge ) && is_callable( array( $GLOBALS['kinsta_cache']->kinsta_cache_purge, 'purge_complete_caches' ) ) ) {
658 $GLOBALS['kinsta_cache']->kinsta_cache_purge->purge_complete_caches(); // @phpstan-ignore-line
659 }
660 // LiteSpeed Cache.
661 if ( is_callable( array( 'LiteSpeed_Cache_Tags', 'add_purge_tag' ) ) ) {
662 LiteSpeed_Cache_Tags::add_purge_tag( '*' ); // @phpstan-ignore-line
663 }
664 // Pagely.
665 if ( class_exists( 'PagelyCachePurge' ) ) {
666 $_pagely = new PagelyCachePurge();
667 if ( is_callable( array( $_pagely, 'purgeAll' ) ) ) {
668 $_pagely->purgeAll();
669 }
670 }
671 // Pressidum.
672 if ( is_callable( array( 'Ninukis_Plugin', 'get_instance' ) ) ) {
673 $_pressidum = Ninukis_Plugin::get_instance(); // @phpstan-ignore-line
674 if ( is_callable( array( $_pressidum, 'purgeAllCaches' ) ) ) {
675 $_pressidum->purgeAllCaches(); // @phpstan-ignore-line
676 }
677 }
678 // Savvii.
679 if ( defined( '\Savvii\CacheFlusherPlugin::NAME_DOMAINFLUSH_NOW' ) ) {
680 $_savvii = new \Savvii\CacheFlusherPlugin(); // @phpstan-ignore-line
681 if ( is_callable( array( $_savvii, 'domainflush' ) ) ) {
682 $_savvii->domainflush(); // @phpstan-ignore-line
683 }
684 }
685 // WP Fastest Cache.
686 if ( isset( $GLOBALS['wp_fastest_cache'] ) && is_callable( array( $GLOBALS['wp_fastest_cache'], 'deleteCache' ) ) ) {
687 $GLOBALS['wp_fastest_cache']->deleteCache( true ); // @phpstan-ignore-line
688 }
689 // WP-Optimize.
690 if ( function_exists( 'WP_Optimize' ) ) {
691 WP_Optimize()->get_page_cache()->purge();
692 }
693 }
694
695 /**
696 * Get the post ID of a given table ID (if the table ID exists).
697 *
698 * @since 1.0.0
699 *
700 * @param string $table_id Table ID.
701 * @return int|false Post ID on success, false on error.
702 */
703 protected function _get_post_id( string $table_id ) /* : int|false */ {
704 $table_post = $this->tables->get( 'table_post' );
705 if ( ! isset( $table_post[ $table_id ] ) ) {
706 return false;
707 }
708 return $table_post[ $table_id ];
709 }
710
711 /**
712 * Update/Add a post ID for a given table ID, and sort the list of tables by their key in natural sort order.
713 *
714 * @since 1.0.0
715 *
716 * @param string $table_id Table ID.
717 * @param int $post_id Post ID.
718 */
719 protected function _update_post_id( string $table_id, int $post_id ): void {
720 $tables = $this->tables->get();
721 $tables['table_post'][ $table_id ] = $post_id;
722 uksort( $tables['table_post'], 'strnatcasecmp' );
723 $this->tables->update( $tables );
724 }
725
726 /**
727 * Remove a table ID/post ID connection from the list of tables.
728 *
729 * @since 1.0.0
730 *
731 * @param string $table_id Table ID.
732 */
733 protected function _remove_post_id( string $table_id ): void {
734 $tables = $this->tables->get();
735 unset( $tables['table_post'][ $table_id ] );
736 $this->tables->update( $tables );
737 }
738
739 /**
740 * Change the table ID of a table.
741 *
742 * @since 1.0.0
743 *
744 * @param string $old_id Old table ID.
745 * @param string $new_id New table ID.
746 * @return bool|WP_Error True on success, WP_Error on error.
747 */
748 public function change_table_id( string $old_id, string $new_id ) /* : true|WP_Error */ {
749 $post_id = $this->_get_post_id( $old_id );
750 if ( false === $post_id ) {
751 return new WP_Error( 'table_change_id_no_post_id_for_table_id', '', $old_id );
752 }
753
754 // Check new ID for correct format (string from letters, numbers, -, and _ only, except the '0' string).
755 if ( empty( $new_id ) || 0 !== preg_match( '/[^a-zA-Z0-9_-]/', $new_id ) ) {
756 return new WP_Error( 'table_change_id_new_id_is_invalid', '', $new_id );
757 }
758
759 if ( $this->table_exists( $new_id ) ) {
760 return new WP_Error( 'table_change_table_new_id_exists', '', $new_id );
761 }
762
763 $this->_update_post_id( $new_id, $post_id );
764 $this->_remove_post_id( $old_id );
765
766 /**
767 * Fires after the ID of a table has been changed.
768 *
769 * @since 1.1.0
770 *
771 * @param string $new_id New ID of the table.
772 * @param string $old_id Old ID of the table.
773 */
774 do_action( 'tablepress_event_changed_table_id', $new_id, $old_id );
775
776 return true;
777 }
778
779 /**
780 * Get an unused table ID (e.g. for a new table).
781 *
782 * @since 1.0.0
783 *
784 * @return string Unused table ID (e.g. for a new table).
785 */
786 protected function _get_new_table_id(): string {
787 $tables = $this->tables->get();
788 // Need to check new ID candidate in a loop, because a higher ID might already be in use, if a table ID was changed manually.
789 do {
790 ++$tables['last_id'];
791 $last_id_string = (string) $tables['last_id'];
792 } while ( $this->table_exists( $last_id_string ) );
793 $this->tables->update( $tables );
794 return $last_id_string;
795 }
796
797 /**
798 * Get the template for an empty table.
799 *
800 * Important: This scheme is versioned via TablePress::table_scheme_version; changes likely need a version update!
801 *
802 * @since 1.0.0
803 *
804 * @return array<string, mixed> Empty table.
805 */
806 public function get_table_template(): array {
807 // Attention: Array keys have to be lowercase, to make it possible to match them with Shortcode attributes!
808 $table = array(
809 'id' => false,
810 'name' => '',
811 'description' => '',
812 'data' => array( array( '' ) ), // One empty cell.
813 'last_modified' => wp_date( 'Y-m-d H:i:s' ),
814 'author' => get_current_user_id(),
815 'options' => array(
816 'last_editor' => get_current_user_id(),
817 'table_head' => true,
818 'table_foot' => false,
819 'alternating_row_colors' => true,
820 'row_hover' => true,
821 'print_name' => false,
822 'print_name_position' => 'above',
823 'print_description' => false,
824 'print_description_position' => 'below',
825 'extra_css_classes' => '',
826 // DataTables JavaScript library.
827 'use_datatables' => true,
828 'datatables_sort' => true,
829 'datatables_filter' => true,
830 'datatables_paginate' => true,
831 'datatables_lengthchange' => true,
832 'datatables_paginate_entries' => 10,
833 'datatables_info' => true,
834 'datatables_scrollx' => false,
835 'datatables_custom_commands' => '',
836 ),
837 'visibility' => array(
838 'rows' => array( 1 ), // One visbile row.
839 'columns' => array( 1 ), // One visible column.
840 ),
841 );
842 /**
843 * Filters the default template/structure of an empty table.
844 *
845 * @since 1.0.0
846 *
847 * @param array<string, mixed> $table Default template/structure of an empty table.
848 */
849 return apply_filters( 'tablepress_table_template', $table );
850 }
851
852 /**
853 * Combine two tables (e.g. an existing one with the updated data, or an empty one with new data).
854 *
855 * Performs consistency checks on data and visibility settings.
856 *
857 * @since 1.0.0
858 *
859 * @param array<string, mixed> $table Table to merge into.
860 * @param array<string, mixed> $new_table Table to merge.
861 * @param bool $table_size_check Optional. Whether to check the number of rows and columns (e.g. not necessary for added or copied tables).
862 * @return array<string, mixed>|WP_Error Merged table on success, WP_Error on error.
863 */
864 public function prepare_table( array $table, array $new_table, bool $table_size_check = true ) /* : array|WP_Error */ {
865 // Table ID must be the same (if there was an ID already).
866 if ( false !== $table['id'] && $table['id'] !== $new_table['id'] ) {
867 return new WP_Error( 'table_prepare_no_id_match', '', $new_table['id'] );
868 }
869
870 // Name, description, and data array need to exist, data must not be empty, the others could be ''.
871 if ( ! isset( $new_table['name'], $new_table['description'] )
872 || empty( $new_table['data'] ) || empty( $new_table['data'][0] ) ) {
873 return new WP_Error( 'table_prepare_name_description_or_data_not_set' );
874 }
875
876 // Visibility needs to exist.
877 if ( ! isset( $new_table['visibility']['rows'], $new_table['visibility']['columns'] ) ) {
878 return new WP_Error( 'table_prepare_visibility_not_set' );
879 }
880 $new_table['visibility']['rows'] = array_map( 'intval', $new_table['visibility']['rows'] );
881 $new_table['visibility']['columns'] = array_map( 'intval', $new_table['visibility']['columns'] );
882
883 // Check dimensions of table data array (not done for newly added, copied, or imported tables).
884 if ( $table_size_check ) {
885 if ( ! isset( $new_table['number']['rows'], $new_table['number']['columns'] ) ) {
886 return new WP_Error( 'table_prepare_size_check_numbers_not_set' );
887 }
888 // Table data needs to be ok, and have the correct number of rows and columns.
889 $new_table['number']['rows'] = (int) $new_table['number']['rows'];
890 $new_table['number']['columns'] = (int) $new_table['number']['columns'];
891 if ( 0 === $new_table['number']['rows']
892 || 0 === $new_table['number']['columns']
893 || count( $new_table['data'] ) !== $new_table['number']['rows']
894 || count( $new_table['data'][0] ) !== $new_table['number']['columns'] ) {
895 return new WP_Error( 'table_prepare_size_check_numbers_dont_match' );
896 }
897 // Visibility also needs to have correct dimensions.
898 if ( count( $new_table['visibility']['rows'] ) !== $new_table['number']['rows']
899 || count( $new_table['visibility']['columns'] ) !== $new_table['number']['columns'] ) {
900 return new WP_Error( 'table_prepare_size_check_visibility_doesnt_match' );
901 }
902 }
903
904 // All checks were successful, replace original values with new ones.
905
906 // $table['id'] is either false (and remains false) or already equal to $new_table['id'].
907 $table['new_id'] = $new_table['new_id'] ?? $table['id'];
908 $table['name'] = $new_table['name'];
909 $table['description'] = $new_table['description'];
910 $table['data'] = $new_table['data'];
911 // Make sure that cells are stored as strings.
912 array_walk_recursive(
913 $table['data'],
914 static function ( /* string|int|float|bool|null */ &$cell_content, int $col_idx ): void {
915 $cell_content = (string) $cell_content;
916 }
917 );
918 // Table Options.
919 if ( isset( $new_table['options'] ) ) { // Options are for example not set for newly added tables.
920 // Specials check for certain options.
921 if ( isset( $new_table['options']['extra_css_classes'] ) ) {
922 $new_table['options']['extra_css_classes'] = explode( ' ', $new_table['options']['extra_css_classes'] );
923 $new_table['options']['extra_css_classes'] = array_map( array( 'TablePress', 'sanitize_css_class' ), $new_table['options']['extra_css_classes'] );
924 $new_table['options']['extra_css_classes'] = array_unique( $new_table['options']['extra_css_classes'] );
925 $new_table['options']['extra_css_classes'] = trim( implode( ' ', $new_table['options']['extra_css_classes'] ) );
926 }
927 if ( isset( $new_table['options']['datatables_paginate_entries'] ) ) {
928 $new_table['options']['datatables_paginate_entries'] = (int) $new_table['options']['datatables_paginate_entries'];
929 if ( $new_table['options']['datatables_paginate_entries'] < 1 ) {
930 $new_table['options']['datatables_paginate_entries'] = 10; // Default value.
931 }
932 }
933 // Merge new options.
934 $default_table = $this->get_table_template();
935 $table['options'] = array_intersect_key( $table['options'], $default_table['options'] );
936 $new_table['options'] = array_intersect_key( $new_table['options'], $default_table['options'] );
937 $table['options'] = array_merge( $table['options'], $new_table['options'] );
938 }
939 // Table Visibility.
940 $table['visibility']['rows'] = $new_table['visibility']['rows'];
941 $table['visibility']['columns'] = $new_table['visibility']['columns'];
942
943 // $table['author'] = get_current_user_id(); // We don't want this, as it would override the original author.
944 // $table['created'] = wp_date( 'Y-m-d H:i:s' ); // We don't want this, as it would override the original datetime.
945 $table['last_modified'] = wp_date( 'Y-m-d H:i:s' );
946 $table['options']['last_editor'] = get_current_user_id();
947
948 return $table;
949 }
950
951 /**
952 * Save the table options of a table (in a post meta field of the table's post).
953 *
954 * @since 1.0.0
955 *
956 * @param int $post_id Post ID.
957 * @param array<string, mixed> $options Table options.
958 * @return bool True on success, false on error.
959 */
960 protected function _add_table_options( int $post_id, array $options ): bool {
961 $options = wp_json_encode( $options, TABLEPRESS_JSON_OPTIONS );
962 return $this->model_post->add_meta_field( $post_id, $this->table_options_field_name, $options ); // @phpstan-ignore-line
963 }
964
965 /**
966 * Update the table options of a table (in a post meta field in the table's post).
967 *
968 * @since 1.0.0
969 *
970 * @param int $post_id Post ID.
971 * @param array<string, mixed> $options Table options.
972 * @return bool True on success, false on error.
973 */
974 protected function _update_table_options( int $post_id, array $options ): bool {
975 $options = wp_json_encode( $options, TABLEPRESS_JSON_OPTIONS );
976 return $this->model_post->update_meta_field( $post_id, $this->table_options_field_name, $options ); // @phpstan-ignore-line
977 }
978
979 /**
980 * Get the table options of a table (from a post meta field of the table's post).
981 *
982 * @since 1.0.0
983 *
984 * @param int $post_id Post ID.
985 * @return array<string, mixed> Table options on success, empty array on error.
986 */
987 protected function _get_table_options( int $post_id ): array {
988 $options = $this->model_post->get_meta_field( $post_id, $this->table_options_field_name );
989 if ( empty( $options ) ) {
990 return array();
991 }
992 return (array) json_decode( $options, true );
993 }
994
995 /**
996 * Save the table visibility of a table (in a post meta field of the table's post).
997 *
998 * @since 1.0.0
999 *
1000 * @param int $post_id Post ID.
1001 * @param array{rows: int[], columns: int[]} $visibility Table visibility.
1002 * @return bool True on success, false on error.
1003 */
1004 protected function _add_table_visibility( int $post_id, array $visibility ): bool {
1005 $visibility = wp_json_encode( $visibility, TABLEPRESS_JSON_OPTIONS );
1006 return $this->model_post->add_meta_field( $post_id, $this->table_visibility_field_name, $visibility ); // @phpstan-ignore-line
1007 }
1008
1009 /**
1010 * Update the table visibility of a table (in a post meta field in the table's post).
1011 *
1012 * @since 1.0.0
1013 *
1014 * @param int $post_id Post ID.
1015 * @param array{rows: int[], columns: int[]} $visibility Table visibility.
1016 * @return bool True on success, false on error.
1017 */
1018 protected function _update_table_visibility( int $post_id, array $visibility ): bool {
1019 $visibility = wp_json_encode( $visibility, TABLEPRESS_JSON_OPTIONS );
1020 return $this->model_post->update_meta_field( $post_id, $this->table_visibility_field_name, $visibility ); // @phpstan-ignore-line
1021 }
1022
1023 /**
1024 * Get the table visibility of a table (from a post meta field of the table's post).
1025 *
1026 * @since 1.0.0
1027 *
1028 * @param int $post_id Post ID.
1029 * @return array{rows: int[], columns: int[]} Table visibility on success, empty array on error.
1030 */
1031 protected function _get_table_visibility( int $post_id ): array {
1032 $visibility = $this->model_post->get_meta_field( $post_id, $this->table_visibility_field_name );
1033 if ( empty( $visibility ) ) {
1034 return array(
1035 'rows' => array(),
1036 'columns' => array(),
1037 );
1038 }
1039 return json_decode( $visibility, true );
1040 }
1041
1042 /**
1043 * Merge existing Table Options with default Table Options,
1044 * remove (no longer) existing options, after a table scheme change,
1045 * for all tables.
1046 *
1047 * @since 1.0.0
1048 * @since 2.0.0 Add optional $remove_old_options parameter.
1049 *
1050 * @param bool $remove_old_options Optional. Whether old table options should be removed from the database. Default true.
1051 */
1052 public function merge_table_options_defaults( bool $remove_old_options = true ): void {
1053 $table_post = $this->tables->get( 'table_post' );
1054 if ( empty( $table_post ) ) {
1055 return;
1056 }
1057
1058 // Prime the meta cache with the table options of all tables.
1059 update_meta_cache( 'post', array_values( $table_post ) );
1060
1061 // Get default Table with default Table Options.
1062 $default_table = $this->get_table_template();
1063
1064 // Go through all tables (this loop now uses the WP cache).
1065 foreach ( $table_post as $post_id ) {
1066 $table_options = $this->_get_table_options( $post_id );
1067 if ( $remove_old_options ) {
1068 // Remove old (i.e. no longer existing) Table Options.
1069 $table_options = array_intersect_key( $table_options, $default_table['options'] );
1070 }
1071 // Merge current into new Table Options.
1072 $table_options = array_merge( $default_table['options'], $table_options );
1073 $this->_update_table_options( $post_id, $table_options );
1074 }
1075 }
1076
1077 /**
1078 * Invalidate all table output caches, e.g. after a plugin update.
1079 *
1080 * @since 1.0.0
1081 */
1082 public function invalidate_table_output_caches(): void {
1083 $table_post = $this->tables->get( 'table_post' );
1084 if ( empty( $table_post ) ) {
1085 return;
1086 }
1087
1088 foreach ( $table_post as $table_id => $post_id ) {
1089 $table_id = (string) $table_id; // Ensure that the table ID is a string, as it comes from an array key where numeric strings are converted to integers.
1090 $this->invalidate_table_output_cache( $table_id );
1091 }
1092 }
1093
1094 /**
1095 * Add mime type field to existing posts with the TablePress Custom Post Type,
1096 * so that other plugins know that they are not dealing with plain text.
1097 *
1098 * @since 1.5.0
1099 *
1100 * @global wpdb $wpdb WordPress database abstraction object.
1101 */
1102 public function add_mime_type_to_posts(): void {
1103 global $wpdb;
1104 $wpdb->update( $wpdb->posts, array( 'post_mime_type' => 'application/json' ), array( 'post_type' => $this->model_post->get_post_type() ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1105 }
1106
1107 /**
1108 * Add a table ID for a post (table) that was imported through the WP WXR importer to the table ID to post ID map.
1109 *
1110 * @since 1.5.0
1111 *
1112 * @param int|WP_Error $post_id Post ID of the imported post on success. 0 or WP_Error on failure.
1113 * @param int $original_post_id Original post ID that the post had on the site where it was exported from.
1114 * @param array<string, mixed> $postdata Post data that was imported into the database.
1115 * @param array<string, mixed> $post Original post data as it was exported.
1116 */
1117 public function add_table_id_on_wp_import( /* int|WP_Error */ $post_id, int $original_post_id, array $postdata, array $post ): void {
1118 // Bail if the post could not be imported or if the post is not a TablePress table.
1119 if ( is_wp_error( $post_id ) || $this->model_post->get_post_type() !== $postdata['post_type'] ) {
1120 return;
1121 }
1122
1123 // Extract the table IDs from the `_tablepress_export_table_id` post meta field.
1124 $table_ids = array();
1125 if ( isset( $post['postmeta'] ) && is_array( $post['postmeta'] ) ) {
1126 foreach ( $post['postmeta'] as $postmeta ) {
1127 if ( '_tablepress_export_table_id' === $postmeta['key'] ) {
1128 $table_ids = $postmeta['value'];
1129 $table_ids = explode( ',', $table_ids );
1130 break;
1131 }
1132 }
1133 }
1134
1135 // Save the post ID for each of the table IDs.
1136 $post_id_saved = false;
1137 foreach ( $table_ids as $table_id ) {
1138 $table_id = (string) preg_replace( '/[^a-zA-Z0-9_-]/', '', $table_id );
1139 if ( '' === $table_id || $this->table_exists( $table_id ) ) {
1140 continue;
1141 }
1142 $this->_update_post_id( $table_id, $post_id );
1143 $post_id_saved = true;
1144 }
1145
1146 // Save the post ID for a new table ID if it could not be saved for any of the imported table IDs.
1147 if ( ! $post_id_saved ) {
1148 $table_id = $this->_get_new_table_id();
1149 $this->_update_post_id( $table_id, $post_id );
1150 }
1151 }
1152
1153 /**
1154 * Remove the `_tablepress_export_table_id` post meta field from the fields that are imported during a WP WXR import.
1155 *
1156 * @since 1.5.0
1157 *
1158 * @param array<string, mixed> $postmeta Post meta fields for the post.
1159 * @param int $post_id Post ID.
1160 * @param array<string, mixed> $post Post.
1161 * @return array<string, mixed> Modified post meta fields.
1162 */
1163 public function prevent_table_id_post_meta_import_on_wp_import( array $postmeta, int $post_id, array $post ): array {
1164 // Bail if the post is not a TablePress table.
1165 if ( $this->model_post->get_post_type() !== $post['post_type'] ) {
1166 return $postmeta;
1167 }
1168
1169 // Remove the `_tablepress_export_table_id` post meta field from the post meta fields.
1170 foreach ( $postmeta as $index => $meta ) {
1171 if ( '_tablepress_export_table_id' === $meta['key'] ) {
1172 unset( $postmeta[ $index ] );
1173 }
1174 }
1175
1176 return $postmeta;
1177 }
1178
1179 /**
1180 * Add the table IDs for an exported post (table) to the WP WXR export file.
1181 *
1182 * The table IDs for a table are exported in a faked post meta field.
1183 * As there's no action for adding extra data to the WXR export file, we hijack the `wxr_export_skip_postmeta` filter hook.
1184 *
1185 * @since 1.5.0
1186 *
1187 * @param bool $skip Whether to skip the current post meta. Default false.
1188 * @param string $meta_key Current meta key.
1189 * @param stdClass $meta Current meta object.
1190 * @return bool Whether to skip the current post meta (unchanged $skip parameter).
1191 */
1192 public function add_table_id_to_wp_export( bool $skip, string $meta_key, stdClass $meta ): bool {
1193 // Bail if the exporter doesn't process a TablePress table right now.
1194 if ( $this->table_options_field_name !== $meta_key ) {
1195 return $skip;
1196 }
1197
1198 // Find all table IDs that map to the post ID of the table that is currently being exported.
1199 $table_post = $this->tables->get( 'table_post' );
1200 $table_ids = array_keys( $table_post, (int) $meta->post_id, true );
1201
1202 // Bail if no table IDs are mapped to this post ID.
1203 if ( empty( $table_ids ) ) {
1204 return $skip;
1205 }
1206
1207 // Pretend that there is a `_tablepress_export_table_id` post meta field with the list of table IDs.
1208 $key = '_tablepress_export_table_id';
1209 $value = wxr_cdata( implode( ',', $table_ids ) ); // @phpstan-ignore-line
1210
1211 // Hijack the filter and print extra XML code for our faked post meta field.
1212 // phpcs:disable WordPress.Security.EscapeOutput.HeredocOutputNotEscaped
1213 echo <<<WXR
1214 <wp:postmeta>
1215 <wp:meta_key>{$key}</wp:meta_key>
1216 <wp:meta_value>{$value}</wp:meta_value>
1217 </wp:postmeta>\n
1218 WXR;
1219 // phpcs:enable
1220
1221 return $skip;
1222 }
1223
1224 /**
1225 * Delete the WP_Option of the model.
1226 *
1227 * @since 1.0.0
1228 */
1229 public function destroy(): void {
1230 $this->tables->delete();
1231 }
1232
1233 } // class TablePress_Table_Model
1234