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

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