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

1,291 lines 44.2 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 // Kinsta.
631 if ( isset( $GLOBALS['kinsta_cache'] ) && ! empty( $GLOBALS['kinsta_cache']->kinsta_cache_purge ) && is_callable( array( $GLOBALS['kinsta_cache']->kinsta_cache_purge, 'purge_complete_caches' ) ) ) {
632 $GLOBALS['kinsta_cache']->kinsta_cache_purge->purge_complete_caches(); // @phpstan-ignore method.nonObject
633 }
634 // LiteSpeed Cache.
635 if ( is_callable( array( 'LiteSpeed_Cache_Tags', 'add_purge_tag' ) ) ) {
636 LiteSpeed_Cache_Tags::add_purge_tag( '*' ); // @phpstan-ignore class.notFound
637 }
638 // Pagely.
639 if ( class_exists( 'PagelyCachePurge' ) ) {
640 $_pagely = new PagelyCachePurge();
641 if ( is_callable( array( $_pagely, 'purgeAll' ) ) ) {
642 $_pagely->purgeAll();
643 }
644 }
645 // Pressidium.
646 if ( is_callable( array( 'Ninukis_Plugin', 'get_instance' ) ) ) {
647 $_pressidum = Ninukis_Plugin::get_instance(); // @phpstan-ignore class.notFound
648 if ( is_callable( array( $_pressidum, 'purgeAllCaches' ) ) ) {
649 $_pressidum->purgeAllCaches(); // @phpstan-ignore method.nonObject
650 }
651 }
652 // Savvii.
653 if ( defined( '\Savvii\CacheFlusherPlugin::NAME_DOMAINFLUSH_NOW' ) ) {
654 $_savvii = new \Savvii\CacheFlusherPlugin(); // @phpstan-ignore class.notFound
655 if ( is_callable( array( $_savvii, 'domainflush' ) ) ) {
656 $_savvii->domainflush(); // @phpstan-ignore class.notFound
657 }
658 }
659 // WP Fastest Cache.
660 if ( isset( $GLOBALS['wp_fastest_cache'] ) && is_callable( array( $GLOBALS['wp_fastest_cache'], 'deleteCache' ) ) ) {
661 $GLOBALS['wp_fastest_cache']->deleteCache( true ); // @phpstan-ignore method.nonObject
662 }
663 // WP-Optimize.
664 if ( function_exists( 'WP_Optimize' ) ) {
665 WP_Optimize()->get_page_cache()->purge();
666 }
667 }
668
669 /**
670 * Get the post ID of a given table ID (if the table ID exists).
671 *
672 * @since 1.0.0
673 *
674 * @param string $table_id Table ID.
675 * @return int|false Post ID on success, false on error.
676 */
677 protected function _get_post_id( string $table_id ) /* : int|false */ {
678 $table_post = $this->tables->get( 'table_post' );
679 if ( ! isset( $table_post[ $table_id ] ) ) {
680 return false;
681 }
682 return $table_post[ $table_id ];
683 }
684
685 /**
686 * Update/Add a post ID for a given table ID, and sort the list of tables by their key in natural sort order.
687 *
688 * @since 1.0.0
689 *
690 * @param string $table_id Table ID.
691 * @param int $post_id Post ID.
692 */
693 protected function _update_post_id( string $table_id, int $post_id ): void {
694 $tables = $this->tables->get();
695 $tables['table_post'][ $table_id ] = $post_id;
696 uksort( $tables['table_post'], 'strnatcasecmp' );
697 $this->tables->update( $tables );
698 }
699
700 /**
701 * Remove a table ID/post ID connection from the list of tables.
702 *
703 * @since 1.0.0
704 *
705 * @param string $table_id Table ID.
706 */
707 protected function _remove_post_id( string $table_id ): void {
708 $tables = $this->tables->get();
709 unset( $tables['table_post'][ $table_id ] );
710 $this->tables->update( $tables );
711 }
712
713 /**
714 * Change the table ID of a table.
715 *
716 * @since 1.0.0
717 *
718 * @param string $old_id Old table ID.
719 * @param string $new_id New table ID.
720 * @return bool|WP_Error True on success, WP_Error on error.
721 */
722 public function change_table_id( string $old_id, string $new_id ) /* : true|WP_Error */ {
723 $post_id = $this->_get_post_id( $old_id );
724 if ( false === $post_id ) {
725 return new WP_Error( 'table_change_id_no_post_id_for_table_id', '', $old_id );
726 }
727
728 // Check new ID for correct format (string from letters, numbers, -, and _ only, except the '0' string).
729 if ( empty( $new_id ) || 0 !== preg_match( '/[^a-zA-Z0-9_-]/', $new_id ) ) {
730 return new WP_Error( 'table_change_id_new_id_is_invalid', '', $new_id );
731 }
732
733 if ( $this->table_exists( $new_id ) ) {
734 return new WP_Error( 'table_change_table_new_id_exists', '', $new_id );
735 }
736
737 $this->_update_post_id( $new_id, $post_id );
738 $this->_remove_post_id( $old_id );
739
740 /**
741 * Fires after the ID of a table has been changed.
742 *
743 * @since 1.1.0
744 *
745 * @param string $new_id New ID of the table.
746 * @param string $old_id Old ID of the table.
747 */
748 do_action( 'tablepress_event_changed_table_id', $new_id, $old_id );
749
750 return true;
751 }
752
753 /**
754 * Get an unused table ID (e.g. for a new table).
755 *
756 * @since 1.0.0
757 *
758 * @return string Unused table ID (e.g. for a new table).
759 */
760 protected function _get_new_table_id(): string {
761 $tables = $this->tables->get();
762 // 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.
763 do {
764 ++$tables['last_id'];
765 $last_id_string = (string) $tables['last_id'];
766 } while ( $this->table_exists( $last_id_string ) );
767 $this->tables->update( $tables );
768 return $last_id_string;
769 }
770
771 /**
772 * Get the template for an empty table.
773 *
774 * Important: This scheme is versioned via TablePress::table_scheme_version; changes likely need a version update!
775 *
776 * @since 1.0.0
777 *
778 * @return array<string, mixed> Empty table.
779 */
780 public function get_table_template(): array {
781 // Attention: Array keys have to be lowercase, to make it possible to match them with Shortcode attributes!
782 $table = array(
783 'id' => false,
784 'name' => '',
785 'description' => '',
786 'data' => array( array( '' ) ), // One empty cell.
787 'last_modified' => wp_date( 'Y-m-d H:i:s' ),
788 'author' => get_current_user_id(),
789 'options' => array(
790 'last_editor' => get_current_user_id(),
791 'table_head' => 1,
792 'table_foot' => 0,
793 'alternating_row_colors' => true,
794 'row_hover' => true,
795 'print_name' => false,
796 'print_name_position' => 'above',
797 'print_description' => false,
798 'print_description_position' => 'below',
799 'extra_css_classes' => '',
800 // DataTables JavaScript library.
801 'use_datatables' => true,
802 'datatables_sort' => true,
803 'datatables_filter' => true,
804 'datatables_paginate' => true,
805 'datatables_lengthchange' => true,
806 'datatables_paginate_entries' => 10,
807 'datatables_info' => true,
808 'datatables_scrollx' => false,
809 'datatables_custom_commands' => '',
810 ),
811 'visibility' => array(
812 'rows' => array( 1 ), // One visible row.
813 'columns' => array( 1 ), // One visible column.
814 ),
815 );
816 /**
817 * Filters the default template/structure of an empty table.
818 *
819 * @since 1.0.0
820 *
821 * @param array<string, mixed> $table Default template/structure of an empty table.
822 */
823 return apply_filters( 'tablepress_table_template', $table );
824 }
825
826 /**
827 * Combine two tables (e.g. an existing one with the updated data, or an empty one with new data).
828 *
829 * Performs consistency checks on data and visibility settings.
830 *
831 * @since 1.0.0
832 *
833 * @param array<string, mixed> $table Table to merge into.
834 * @param array<string, mixed> $new_table Table to merge.
835 * @param bool $table_size_check Optional. Whether to check the number of rows and columns (e.g. not necessary for added or copied tables).
836 * @return array<string, mixed>|WP_Error Merged table on success, WP_Error on error.
837 */
838 public function prepare_table( array $table, array $new_table, bool $table_size_check = true ) /* : array|WP_Error */ {
839 // Table ID must be the same (if there was an ID already).
840 if ( false !== $table['id'] && $table['id'] !== $new_table['id'] ) {
841 return new WP_Error( 'table_prepare_no_id_match', '', $new_table['id'] );
842 }
843
844 // Name, description, and data array need to exist, data must not be empty, the others could be ''.
845 if ( ! isset( $new_table['name'], $new_table['description'] )
846 || empty( $new_table['data'] ) || empty( $new_table['data'][0] ) ) {
847 return new WP_Error( 'table_prepare_name_description_or_data_not_set' );
848 }
849
850 // Visibility needs to exist.
851 if ( ! isset( $new_table['visibility']['rows'], $new_table['visibility']['columns'] ) ) {
852 return new WP_Error( 'table_prepare_visibility_not_set' );
853 }
854 $new_table['visibility']['rows'] = array_map( 'intval', $new_table['visibility']['rows'] );
855 $new_table['visibility']['columns'] = array_map( 'intval', $new_table['visibility']['columns'] );
856
857 // Check dimensions of table data array (not done for newly added, copied, or imported tables).
858 if ( $table_size_check ) {
859 if ( ! isset( $new_table['number']['rows'], $new_table['number']['columns'] ) ) {
860 return new WP_Error( 'table_prepare_size_check_numbers_not_set' );
861 }
862 // Table data needs to be ok, and have the correct number of rows and columns.
863 $new_table['number']['rows'] = (int) $new_table['number']['rows'];
864 $new_table['number']['columns'] = (int) $new_table['number']['columns'];
865 if ( 0 === $new_table['number']['rows']
866 || 0 === $new_table['number']['columns']
867 || count( $new_table['data'] ) !== $new_table['number']['rows']
868 || count( $new_table['data'][0] ) !== $new_table['number']['columns'] ) {
869 return new WP_Error( 'table_prepare_size_check_numbers_dont_match' );
870 }
871 // Visibility also needs to have correct dimensions.
872 if ( count( $new_table['visibility']['rows'] ) !== $new_table['number']['rows']
873 || count( $new_table['visibility']['columns'] ) !== $new_table['number']['columns'] ) {
874 return new WP_Error( 'table_prepare_size_check_visibility_doesnt_match' );
875 }
876 }
877
878 // All checks were successful, replace original values with new ones.
879
880 // $table['id'] is either false (and remains false) or already equal to $new_table['id'].
881 $table['new_id'] = $new_table['new_id'] ?? $table['id'];
882 $table['name'] = $new_table['name'];
883 $table['description'] = $new_table['description'];
884 $table['data'] = $new_table['data'];
885 // Make sure that cells are stored as strings.
886 array_walk_recursive(
887 $table['data'],
888 static function ( /* string|int|float|bool|null */ &$cell_content, int $col_idx ): void {
889 $cell_content = (string) $cell_content;
890 },
891 );
892 // Table Options.
893 if ( isset( $new_table['options'] ) ) { // Options are for example not set for newly added tables.
894 // Specials check for certain options.
895 if ( isset( $new_table['options']['extra_css_classes'] ) ) {
896 $new_table['options']['extra_css_classes'] = explode( ' ', $new_table['options']['extra_css_classes'] );
897 $new_table['options']['extra_css_classes'] = array_map( array( 'TablePress', 'sanitize_css_class' ), $new_table['options']['extra_css_classes'] );
898 $new_table['options']['extra_css_classes'] = array_unique( $new_table['options']['extra_css_classes'] );
899 $new_table['options']['extra_css_classes'] = trim( implode( ' ', $new_table['options']['extra_css_classes'] ) );
900 }
901 if ( isset( $new_table['options']['datatables_paginate_entries'] ) ) {
902 $new_table['options']['datatables_paginate_entries'] = (int) $new_table['options']['datatables_paginate_entries'];
903 if ( $new_table['options']['datatables_paginate_entries'] < 1 ) {
904 $new_table['options']['datatables_paginate_entries'] = 10; // Default value.
905 }
906 }
907
908 // Backward compatibility: Convert boolean or numeric string "table_head" and "table_foot" options to integer.
909 if ( isset( $new_table['options']['table_head'] ) ) {
910 $new_table['options']['table_head'] = absint( $new_table['options']['table_head'] );
911 }
912 if ( isset( $new_table['options']['table_foot'] ) ) {
913 $new_table['options']['table_foot'] = absint( $new_table['options']['table_foot'] );
914 }
915
916 // Merge new options.
917 $default_table = $this->get_table_template();
918 $table['options'] = array_intersect_key( $table['options'], $default_table['options'] );
919 $new_table['options'] = array_intersect_key( $new_table['options'], $default_table['options'] );
920 $table['options'] = array_merge( $table['options'], $new_table['options'] );
921 }
922 // Table Visibility.
923 $table['visibility']['rows'] = $new_table['visibility']['rows'];
924 $table['visibility']['columns'] = $new_table['visibility']['columns'];
925
926 // $table['author'] = get_current_user_id(); // We don't want this, as it would override the original author.
927 // $table['created'] = wp_date( 'Y-m-d H:i:s' ); // We don't want this, as it would override the original datetime.
928 $table['last_modified'] = wp_date( 'Y-m-d H:i:s' );
929 $table['options']['last_editor'] = get_current_user_id();
930
931 // Prevent issues if the "Custom Commands" field is not set, e.g. when non-admins have previously edited the table.
932 if ( ! isset( $table['options']['datatables_custom_commands'] ) ) {
933 $table['options']['datatables_custom_commands'] = '';
934 }
935
936 // Convert CSS classes and some DataTables 1.x parameters to the DataTables 2 variants.
937 if ( '' !== $table['options']['datatables_custom_commands'] ) {
938 $table['options']['datatables_custom_commands'] = TablePress::convert_datatables_api_data( $table['options']['datatables_custom_commands'] );
939 }
940
941 return $table;
942 }
943
944 /**
945 * Save the table options of a table (in a post meta field of the table's post).
946 *
947 * @since 1.0.0
948 *
949 * @param int $post_id Post ID.
950 * @param array<string, mixed> $options Table options.
951 * @return bool True on success, false on error.
952 */
953 protected function _add_table_options( int $post_id, array $options ): bool {
954 $options = wp_json_encode( $options, TABLEPRESS_JSON_OPTIONS );
955 return $this->model_post->add_meta_field( $post_id, $this->table_options_field_name, $options ); // @phpstan-ignore argument.type
956 }
957
958 /**
959 * Update the table options of a table (in a post meta field in the table's post).
960 *
961 * @since 1.0.0
962 *
963 * @param int $post_id Post ID.
964 * @param array<string, mixed> $options Table options.
965 * @return bool True on success, false on error.
966 */
967 protected function _update_table_options( int $post_id, array $options ): bool {
968 $options = wp_json_encode( $options, TABLEPRESS_JSON_OPTIONS );
969 return $this->model_post->update_meta_field( $post_id, $this->table_options_field_name, $options ); // @phpstan-ignore argument.type
970 }
971
972 /**
973 * Get the table options of a table (from a post meta field of the table's post).
974 *
975 * @since 1.0.0
976 *
977 * @param int $post_id Post ID.
978 * @return array<string, mixed> Table options on success, empty array on error.
979 */
980 protected function _get_table_options( int $post_id ): array {
981 $options = $this->model_post->get_meta_field( $post_id, $this->table_options_field_name );
982 if ( empty( $options ) ) {
983 return array();
984 }
985 $options = (array) json_decode( $options, true );
986
987 // Backward compatibility: Convert boolean "table_head" and "table_foot" options to integer.
988 $options['table_head'] = absint( $options['table_head'] );
989 $options['table_foot'] = absint( $options['table_foot'] );
990
991 return $options;
992 }
993
994 /**
995 * Save the table visibility of a table (in a post meta field of the table's post).
996 *
997 * @since 1.0.0
998 *
999 * @param int $post_id Post ID.
1000 * @param array{rows: int[], columns: int[]} $visibility Table visibility.
1001 * @return bool True on success, false on error.
1002 */
1003 protected function _add_table_visibility( int $post_id, array $visibility ): bool {
1004 $visibility = wp_json_encode( $visibility, TABLEPRESS_JSON_OPTIONS );
1005 return $this->model_post->add_meta_field( $post_id, $this->table_visibility_field_name, $visibility ); // @phpstan-ignore argument.type
1006 }
1007
1008 /**
1009 * Update the table visibility of a table (in a post meta field in the table's post).
1010 *
1011 * @since 1.0.0
1012 *
1013 * @param int $post_id Post ID.
1014 * @param array{rows: int[], columns: int[]} $visibility Table visibility.
1015 * @return bool True on success, false on error.
1016 */
1017 protected function _update_table_visibility( int $post_id, array $visibility ): bool {
1018 $visibility = wp_json_encode( $visibility, TABLEPRESS_JSON_OPTIONS );
1019 return $this->model_post->update_meta_field( $post_id, $this->table_visibility_field_name, $visibility ); // @phpstan-ignore argument.type
1020 }
1021
1022 /**
1023 * Get the table visibility of a table (from a post meta field of the table's post).
1024 *
1025 * @since 1.0.0
1026 *
1027 * @param int $post_id Post ID.
1028 * @return array{rows: int[], columns: int[]} Table visibility on success, empty array on error.
1029 */
1030 protected function _get_table_visibility( int $post_id ): array {
1031 $visibility = $this->model_post->get_meta_field( $post_id, $this->table_visibility_field_name );
1032 if ( empty( $visibility ) ) {
1033 return array(
1034 'rows' => array(),
1035 'columns' => array(),
1036 );
1037 }
1038 return json_decode( $visibility, true );
1039 }
1040
1041 /**
1042 * Merge existing Table Options with default Table Options,
1043 * remove (no longer) existing options, after a table scheme change,
1044 * for all tables.
1045 *
1046 * @since 1.0.0
1047 * @since 2.0.0 Add optional $remove_old_options parameter.
1048 *
1049 * @param bool $remove_old_options Optional. Whether old table options should be removed from the database. Default true.
1050 */
1051 public function merge_table_options_defaults( bool $remove_old_options = true ): void {
1052 $table_post = $this->tables->get( 'table_post' );
1053 if ( empty( $table_post ) ) {
1054 return;
1055 }
1056
1057 // Prime the meta cache with the table options of all tables.
1058 update_meta_cache( 'post', array_values( $table_post ) );
1059
1060 // Get default Table with default Table Options.
1061 $default_table = $this->get_table_template();
1062
1063 // Go through all tables (this loop now uses the WP cache).
1064 foreach ( $table_post as $post_id ) {
1065 $table_options = $this->_get_table_options( $post_id );
1066
1067 /**
1068 * Filters the Table Options before they are merged with the default Table Options.
1069 *
1070 * @since 3.0.0
1071 *
1072 * @param array<string, mixed> $table_options Table Options.
1073 * @param array<string, mixed> $default_table_options Default Table Options.
1074 * @param bool $remove_old_options Whether old table options should be removed from the database.
1075 * @param int $post_id Post ID of the table.
1076 */
1077 $table_options = apply_filters( 'tablepress_table_options_before_merge', $table_options, $default_table['options'], $remove_old_options, $post_id );
1078
1079 if ( $remove_old_options ) {
1080 // Remove old (i.e. no longer existing) Table Options.
1081 $table_options = array_intersect_key( $table_options, $default_table['options'] );
1082 }
1083 // Merge current into new Table Options.
1084 $table_options = array_merge( $default_table['options'], $table_options );
1085 $this->_update_table_options( $post_id, $table_options );
1086 }
1087 }
1088
1089 /**
1090 * Updates all tables' "Custom Commands" to use DataTables 2 variants instead of old DataTables 1.x CSS classes and parameters
1091 *
1092 * @since 3.0.0
1093 */
1094 public function update_custom_commands_datatables_tp30(): void {
1095 $table_post = $this->tables->get( 'table_post' );
1096 if ( empty( $table_post ) ) {
1097 return;
1098 }
1099
1100 // Prime the meta cache with the table options of all tables.
1101 update_meta_cache( 'post', array_values( $table_post ) );
1102
1103 foreach ( $table_post as $table_id => $post_id ) {
1104 $table_options = $this->_get_table_options( $post_id );
1105
1106 // Fix tables where the "Custom Commands" entry is missing entirely.
1107 if ( ! isset( $table_options['datatables_custom_commands'] ) ) {
1108 $table_options['datatables_custom_commands'] = '';
1109 $this->_update_table_options( $post_id, $table_options );
1110 continue;
1111 }
1112
1113 // Nothing to do if there are no "Custom Commands".
1114 if ( '' === $table_options['datatables_custom_commands'] ) {
1115 continue;
1116 }
1117 // Run search/replace.
1118 $old_custom_commands = $table_options['datatables_custom_commands'];
1119 $table_options['datatables_custom_commands'] = TablePress::convert_datatables_api_data( $table_options['datatables_custom_commands'] );
1120 // No need to save (which runs a DB query) if nothing was replaced in the "Custom Commands".
1121 if ( $old_custom_commands === $table_options['datatables_custom_commands'] ) {
1122 continue;
1123 }
1124
1125 $this->_update_table_options( $post_id, $table_options );
1126 }
1127 }
1128
1129 /**
1130 * Invalidate all table output caches, e.g. after a plugin update.
1131 *
1132 * @since 1.0.0
1133 */
1134 public function invalidate_table_output_caches(): void {
1135 $table_post = $this->tables->get( 'table_post' );
1136 if ( empty( $table_post ) ) {
1137 return;
1138 }
1139
1140 foreach ( $table_post as $table_id => $post_id ) {
1141 $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.
1142 $this->invalidate_table_output_cache( $table_id );
1143 }
1144 }
1145
1146 /**
1147 * Add mime type field to existing posts with the TablePress Custom Post Type,
1148 * so that other plugins know that they are not dealing with plain text.
1149 *
1150 * @since 1.5.0
1151 *
1152 * @global wpdb $wpdb WordPress database abstraction object.
1153 */
1154 public function add_mime_type_to_posts(): void {
1155 global $wpdb;
1156 $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
1157 }
1158
1159 /**
1160 * Add a table ID for a post (table) that was imported through the WP WXR importer to the table ID to post ID map.
1161 *
1162 * @since 1.5.0
1163 *
1164 * @param int|WP_Error $post_id Post ID of the imported post on success. 0 or WP_Error on failure.
1165 * @param int $original_post_id Original post ID that the post had on the site where it was exported from.
1166 * @param array<string, mixed> $postdata Post data that was imported into the database.
1167 * @param array<string, mixed> $post Original post data as it was exported.
1168 */
1169 public function add_table_id_on_wp_import( /* int|WP_Error */ $post_id, int $original_post_id, array $postdata, array $post ): void {
1170 // Bail if the post could not be imported or if the post is not a TablePress table.
1171 if ( is_wp_error( $post_id ) || $this->model_post->get_post_type() !== $postdata['post_type'] ) {
1172 return;
1173 }
1174
1175 // Extract the table IDs from the `_tablepress_export_table_id` post meta field.
1176 $table_ids = array();
1177 if ( isset( $post['postmeta'] ) && is_array( $post['postmeta'] ) ) {
1178 foreach ( $post['postmeta'] as $postmeta ) {
1179 if ( '_tablepress_export_table_id' === $postmeta['key'] ) {
1180 $table_ids = $postmeta['value'];
1181 $table_ids = explode( ',', $table_ids );
1182 break;
1183 }
1184 }
1185 }
1186
1187 // Save the post ID for each of the table IDs.
1188 $post_id_saved = false;
1189 foreach ( $table_ids as $table_id ) {
1190 $table_id = (string) preg_replace( '/[^a-zA-Z0-9_-]/', '', $table_id );
1191 if ( '' === $table_id || $this->table_exists( $table_id ) ) {
1192 continue;
1193 }
1194 $this->_update_post_id( $table_id, $post_id );
1195 $post_id_saved = true;
1196 }
1197
1198 // Save the post ID for a new table ID if it could not be saved for any of the imported table IDs.
1199 if ( ! $post_id_saved ) {
1200 $table_id = $this->_get_new_table_id();
1201 $this->_update_post_id( $table_id, $post_id );
1202 }
1203 }
1204
1205 /**
1206 * Remove the `_tablepress_export_table_id` post meta field from the fields that are imported during a WP WXR import.
1207 *
1208 * @since 1.5.0
1209 *
1210 * @param array<string, mixed> $postmeta Post meta fields for the post.
1211 * @param int $post_id Post ID.
1212 * @param array<string, mixed> $post Post.
1213 * @return array<string, mixed> Modified post meta fields.
1214 */
1215 public function prevent_table_id_post_meta_import_on_wp_import( array $postmeta, int $post_id, array $post ): array {
1216 // Bail if the post is not a TablePress table.
1217 if ( $this->model_post->get_post_type() !== $post['post_type'] ) {
1218 return $postmeta;
1219 }
1220
1221 // Remove the `_tablepress_export_table_id` post meta field from the post meta fields.
1222 foreach ( $postmeta as $index => $meta ) {
1223 if ( '_tablepress_export_table_id' === $meta['key'] ) {
1224 unset( $postmeta[ $index ] );
1225 }
1226 }
1227
1228 return $postmeta;
1229 }
1230
1231 /**
1232 * Add the table IDs for an exported post (table) to the WP WXR export file.
1233 *
1234 * The table IDs for a table are exported in a faked post meta field.
1235 * As there's no action for adding extra data to the WXR export file, we hijack the `wxr_export_skip_postmeta` filter hook.
1236 *
1237 * @since 1.5.0
1238 *
1239 * @param bool $skip Whether to skip the current post meta. Default false.
1240 * @param string $meta_key Current meta key.
1241 * @param stdClass $meta Current meta object.
1242 * @return bool Whether to skip the current post meta (unchanged $skip parameter).
1243 */
1244 public function add_table_id_to_wp_export( bool $skip, string $meta_key, stdClass $meta ): bool {
1245 // Bail if the exporter doesn't process a TablePress table right now.
1246 if ( $this->table_options_field_name !== $meta_key ) {
1247 return $skip;
1248 }
1249
1250 // Find all table IDs that map to the post ID of the table that is currently being exported.
1251 $table_post = $this->tables->get( 'table_post' );
1252 $table_ids = array_keys( $table_post, (int) $meta->post_id, true );
1253
1254 // Bail if no table IDs are mapped to this post ID.
1255 if ( empty( $table_ids ) ) {
1256 return $skip;
1257 }
1258
1259 // Pretend that there is a `_tablepress_export_table_id` post meta field with the list of table IDs.
1260 $key = '_tablepress_export_table_id';
1261
1262 /**
1263 * Load WP export functions.
1264 */
1265 require_once ABSPATH . 'wp-admin/includes/export.php';
1266 $value = wxr_cdata( implode( ',', $table_ids ) );
1267
1268 // Hijack the filter and print extra XML code for our faked post meta field.
1269 // phpcs:disable WordPress.Security.EscapeOutput.HeredocOutputNotEscaped
1270 echo <<<WXR
1271 <wp:postmeta>
1272 <wp:meta_key>{$key}</wp:meta_key>
1273 <wp:meta_value>{$value}</wp:meta_value>
1274 </wp:postmeta>\n
1275 WXR;
1276 // phpcs:enable
1277
1278 return $skip;
1279 }
1280
1281 /**
1282 * Delete the WP_Option of the model.
1283 *
1284 * @since 1.0.0
1285 */
1286 public function destroy(): void {
1287 $this->tables->delete();
1288 }
1289
1290 } // class TablePress_Table_Model
1291