PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 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 All 45 releases
tablepress / models / model-table.php

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

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