PluginProbe
TablePress – Tables in WordPress made easy / 3.2.2
TablePress – Tables in WordPress made easy v3.2.2
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 3.2.2, at models/model-table.php

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