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

1,322 lines 43.1 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 *
407 * @since 1.10.0
408 *
409 * @param array $table Table.
410 * @return array Filtered/modified table.
411 */
412 public function filter_content( array $table ) {
413 /**
414 * Filter whether the contents of table cells and fields should be filtered/modified.
415 *
416 * @since 1.10.0
417 *
418 * @param bool $filter Whether to filter the content of table cells and other fields. Default true.
419 */
420 if ( ! apply_filters( 'tablepress_filter_table_cell_content', true ) ) {
421 return;
422 }
423
424 // Filter the table name and description.
425 $fields = array( 'name', 'description' );
426 foreach ( $fields as $field ) {
427 $table[ $field ] = wp_targeted_link_rel( $table[ $field ] );
428 }
429
430 foreach ( $table['data'] as $row_idx => $row ) {
431 foreach ( $row as $column_idx => $cell_content ) {
432 $table['data'][ $row_idx ][ $column_idx ] = wp_targeted_link_rel( $cell_content );
433 }
434 }
435
436 return $table;
437 }
438
439 /**
440 * Save a table.
441 *
442 * @since 1.0.0
443 *
444 * @param array $table Table (needs to have $table['id']!).
445 * @return string|WP_Error WP_Error on error, string table ID on success.
446 */
447 public function save( array $table ) {
448 if ( empty( $table['id'] ) ) {
449 return new WP_Error( 'table_save_empty_table_id' );
450 }
451
452 $post_id = $this->_get_post_id( $table['id'] );
453 if ( false === $post_id ) {
454 return new WP_Error( 'table_save_no_post_id_for_table_id', '', $table['id'] );
455 }
456
457 $post = $this->_table_to_post( $table, $post_id );
458 $new_post_id = $this->model_post->update( $post );
459 if ( is_wp_error( $new_post_id ) ) {
460 // Add an error code to the existing WP_Error.
461 $new_post_id->add( 'table_save_post_update', '', $post_id );
462 return $new_post_id;
463 }
464 if ( $post_id !== $new_post_id ) {
465 return new WP_Error( 'table_save_new_post_id_does_not_match', '', $new_post_id );
466 }
467
468 $options_saved = $this->_update_table_options( $new_post_id, $table['options'] );
469 if ( ! $options_saved ) {
470 return new WP_Error( 'table_save_update_table_options_failed', '', $new_post_id );
471 }
472
473 $visibility_saved = $this->_update_table_visibility( $new_post_id, $table['visibility'] );
474 if ( ! $visibility_saved ) {
475 return new WP_Error( 'table_save_update_table_visibility_failed', '', $new_post_id );
476 }
477
478 // At this point, post was successfully added.
479
480 // Invalidate table output caches that belong to this table.
481 $this->invalidate_table_output_cache( $table['id'] );
482 // Flush caching plugins' caches.
483 $this->_flush_caching_plugins_caches();
484
485 /**
486 * Fires after a table has been saved.
487 *
488 * @since 1.5.0
489 *
490 * @param string $table_id ID of the added table.
491 */
492 do_action( 'tablepress_event_saved_table', $table['id'] );
493
494 return $table['id'];
495 }
496
497 /**
498 * Add a new table.
499 *
500 * @since 1.0.0
501 *
502 * @param array $table Table ($table['id'] is not necessary).
503 * @param string $copy_or_add Optional. 'copy' if the table is copied, 'add' if it is a new table. Default 'add'.
504 * @return string|WP_Error WP_Error on error, string table ID of the new table on success.
505 */
506 public function add( array $table, $copy_or_add = 'add' ) {
507 $post_id = -1; // to insert table
508 $post = $this->_table_to_post( $table, $post_id );
509 $new_post_id = $this->model_post->insert( $post );
510 if ( is_wp_error( $new_post_id ) ) {
511 // Add an error code to the existing WP_Error.
512 $new_post_id->add( 'table_add_post_insert', '' );
513 return $new_post_id;
514 }
515
516 $options_saved = $this->_add_table_options( $new_post_id, $table['options'] );
517 if ( ! $options_saved ) {
518 return new WP_Error( 'table_add_update_table_options_failed', '', $new_post_id );
519 }
520
521 $visibility_saved = $this->_add_table_visibility( $new_post_id, $table['visibility'] );
522 if ( ! $visibility_saved ) {
523 return new WP_Error( 'table_add_update_table_visibility_failed', '', $new_post_id );
524 }
525
526 // At this point, post was successfully added, now get an unused table ID.
527 $table_id = $this->_get_new_table_id();
528 $this->_update_post_id( $table_id, $new_post_id );
529
530 if ( 'add' === $copy_or_add ) {
531 /**
532 * Fires after a new table has been added.
533 *
534 * @since 1.1.0
535 *
536 * @param string $table_id ID of the added table.
537 */
538 do_action( 'tablepress_event_added_table', $table_id );
539 }
540
541 return $table_id;
542 }
543
544 /**
545 * Create a copy of a table and add it.
546 *
547 * @since 1.0.0
548 *
549 * @param string $table_id ID of the table to be copied.
550 * @return string|WP_Error WP_Error on error, string table ID of the new table on success.
551 */
552 public function copy( $table_id ) {
553 $table = $this->load( $table_id, true, true );
554 if ( is_wp_error( $table ) ) {
555 // Add an error code to the existing WP_Error.
556 $table->add( 'table_copy_table_load', '', $table_id );
557 return $table;
558 }
559
560 // Adjust name of copied table.
561 if ( '' === trim( $table['name'] ) ) {
562 $table['name'] = __( '(no name)', 'tablepress' );
563 }
564 $table['name'] = sprintf( __( 'Copy of %s', 'tablepress' ), $table['name'] );
565
566 // Merge this data into an empty table template.
567 $table = $this->prepare_table( $this->get_table_template(), $table, false );
568 if ( is_wp_error( $table ) ) {
569 // Add an error code to the existing WP_Error.
570 $table->add( 'table_copy_table_prepare', '', $table_id );
571 return $table;
572 }
573
574 // Add the copied table.
575 $new_table_id = $this->add( $table, 'copy' );
576 if ( is_wp_error( $new_table_id ) ) {
577 // Add an error code to the existing WP_Error.
578 $new_table_id->add( 'table_copy_table_add', '', $table_id );
579 return $new_table_id;
580 }
581
582 /**
583 * Fires after an existing table has been copied.
584 *
585 * @since 1.1.0
586 *
587 * @param string $new_table_id ID of the copy of the table.
588 * @param string $table_id ID of the existing table that is copied.
589 */
590 do_action( 'tablepress_event_copied_table', $new_table_id, $table_id );
591
592 return $new_table_id;
593 }
594
595 /**
596 * Delete a table (and its options).
597 *
598 * @since 1.0.0
599 *
600 * @param string $table_id ID of the table to be deleted.
601 * @return bool|WP_Error WP_Error on error, true on success.
602 */
603 public function delete( $table_id ) {
604 if ( ! $this->table_exists( $table_id ) ) {
605 return new WP_Error( 'table_delete_table_does_not_exist', '', $table_id );
606 }
607
608 $post_id = $this->_get_post_id( $table_id ); // No ! false check necessary, as this is covered by table_exists() check above.
609 $deleted = $this->model_post->delete( $post_id ); // Post Meta fields will be deleted automatically by that function.
610 if ( false === $deleted ) {
611 return new WP_Error( 'table_delete_post_could_not_be_deleted', '', $post_id );
612 }
613
614 // If post was deleted successfully, remove the table ID from the list of tables.
615 $this->_remove_post_id( $table_id );
616
617 // Invalidate table output caches that belong to this table.
618 $this->invalidate_table_output_cache( $table_id );
619 // Flush caching plugins' caches.
620 $this->_flush_caching_plugins_caches();
621
622 /**
623 * Fires after a table has been deleted.
624 *
625 * @since 1.1.0
626 *
627 * @param string $table_id ID of the deleted table.
628 */
629 do_action( 'tablepress_event_deleted_table', $table_id );
630
631 return true;
632 }
633
634 /**
635 * Delete all tables.
636 *
637 * @since 1.0.0
638 */
639 public function delete_all() {
640 $tables = $this->tables->get();
641 if ( empty( $tables['table_post'] ) ) {
642 return;
643 }
644
645 foreach ( $tables['table_post'] as $table_id => $post_id ) {
646 $table_id = (string) $table_id;
647 $this->model_post->delete( $post_id ); // Post Meta fields will be deleted automatically by that function.
648 unset( $tables['table_post'][ $table_id ] );
649 // Invalidate table output caches that belong to this table.
650 $this->invalidate_table_output_cache( $table_id );
651 }
652
653 $this->tables->update( $tables );
654 // Flush caching plugins' caches.
655 $this->_flush_caching_plugins_caches();
656
657 /**
658 * Fires after all tables have been deleted.
659 *
660 * @since 1.1.0
661 */
662 do_action( 'tablepress_event_deleted_all_tables' );
663 }
664
665 /**
666 * Check if a table ID exists in the list of tables (this does not guarantee that the post with the table data exists!).
667 *
668 * @since 1.0.0
669 *
670 * @param string $table_id Table ID.
671 * @return bool Whether the table ID exists.
672 */
673 public function table_exists( $table_id ) {
674 $table_post = $this->tables->get( 'table_post' );
675 return isset( $table_post[ $table_id ] );
676 }
677
678 /**
679 * Count the number of tables from either just the list, or by also counting the posts in the database.
680 *
681 * @since 1.0.0
682 *
683 * @param bool $single_value Optional. Whether to return just the number of tables from the list, or also count in the database.
684 * @return bool int|array Number of Tables (if $single_value), or array of Numbers from list/DB (if ! $single_value).
685 */
686 public function count_tables( $single_value = true ) {
687 $count_list = count( $this->tables->get( 'table_post' ) );
688 if ( $single_value ) {
689 return $count_list;
690 }
691
692 $count_db = $this->model_post->count_posts();
693 return array(
694 'list' => $count_list,
695 'db' => $count_db,
696 );
697 }
698
699 /**
700 * Delete all transients used for output caching of a table (e.g. when the table is updated or deleted).
701 *
702 * @since 1.0.0
703 * @since 1.8.0 Renamed from _invalidate_table_output_cache to invalidate_table_output_cache and made public.
704 *
705 * @param string $table_id Table ID.
706 */
707 public function invalidate_table_output_cache( $table_id ) {
708 $caches_list_transient_name = 'tablepress_c_' . md5( $table_id );
709 $caches_list = get_transient( $caches_list_transient_name );
710 if ( false !== $caches_list ) {
711 $caches_list = (array) json_decode( $caches_list, true );
712 foreach ( $caches_list as $cache_transient_name ) {
713 delete_transient( $cache_transient_name );
714 }
715 }
716 delete_transient( $caches_list_transient_name );
717 }
718
719 /**
720 * Flush the caches of the plugins W3 Total Cache, WP Super Cache, Cachify, and Quick Cache.
721 *
722 * @since 1.0.0
723 */
724 public function _flush_caching_plugins_caches() {
725 /**
726 * Filter whether the caches of common caching plugins shall be flushed.
727 *
728 * @since 1.0.0
729 *
730 * @param bool $flush Whether caches of caching plugins shall be flushed. Default true.
731 */
732 if ( ! apply_filters( 'tablepress_flush_caching_plugins_caches', true ) ) {
733 return;
734 }
735
736 // W3 Total Cache
737 if ( function_exists( 'w3tc_pgcache_flush' ) ) {
738 w3tc_pgcache_flush();
739 }
740 // WP Super Cache
741 if ( function_exists( 'wp_cache_clear_cache' ) ) {
742 wp_cache_clear_cache();
743 }
744 // Cachify
745 do_action( 'cachify_flush_cache' );
746 // Quick Cache
747 if ( isset( $GLOBALS['quick_cache'] ) && method_exists( $GLOBALS['quick_cache'], 'clear_cache' ) ) {
748 $GLOBALS['quick_cache']->clear_cache();
749 }
750 // WP Fastest Cache
751 if ( isset( $GLOBALS['wp_fastest_cache'] ) && method_exists( $GLOBALS['wp_fastest_cache'], 'deleteCache' ) ) {
752 $GLOBALS['wp_fastest_cache']->deleteCache();
753 }
754 }
755
756 /**
757 * Get the post ID of a given table ID (if the table ID exists).
758 *
759 * @since 1.0.0
760 *
761 * @param string $table_id Table ID.
762 * @return int|bool Post ID on success, false on error.
763 */
764 protected function _get_post_id( $table_id ) {
765 $table_post = $this->tables->get( 'table_post' );
766 if ( isset( $table_post[ $table_id ] ) ) {
767 return $table_post[ $table_id ];
768 } else {
769 return false;
770 }
771 }
772
773 /**
774 * Update/Add a post ID for a given table ID, and sort the list of tables by their key in natural sort order.
775 *
776 * @since 1.0.0
777 *
778 * @param string $table_id Table ID.
779 * @param int $post_id Post ID.
780 */
781 protected function _update_post_id( $table_id, $post_id ) {
782 $tables = $this->tables->get();
783 $tables['table_post'][ $table_id ] = $post_id;
784 uksort( $tables['table_post'], 'strnatcasecmp' );
785 $this->tables->update( $tables );
786 }
787
788 /**
789 * Remove a table ID/post ID connection from the list of tables.
790 *
791 * @since 1.0.0
792 *
793 * @param string $table_id Table ID.
794 */
795 protected function _remove_post_id( $table_id ) {
796 $tables = $this->tables->get();
797 unset( $tables['table_post'][ $table_id ] );
798 $this->tables->update( $tables );
799 }
800
801 /**
802 * Change the table ID of a table.
803 *
804 * @since 1.0.0
805 *
806 * @param string $old_id Old table ID.
807 * @param string $new_id New table ID.
808 * @return bool|WP_Error True on success, WP_Error on error.
809 */
810 public function change_table_id( $old_id, $new_id ) {
811 $post_id = $this->_get_post_id( $old_id );
812 if ( false === $post_id ) {
813 return new WP_Error( 'table_change_id_no_post_id_for_table_id', '', $old_id );
814 }
815
816 // Check new ID for correct format (string from letters, numbers, -, and _ only, except the '0' string).
817 if ( empty( $new_id ) || 0 !== preg_match( '/[^a-zA-Z0-9_-]/', $new_id ) ) {
818 return new WP_Error( 'table_change_id_new_id_is_invalid', '', $new_id );
819 }
820
821 if ( $this->table_exists( $new_id ) ) {
822 return new WP_Error( 'table_change_table_new_id_exists', '', $new_id );
823 }
824
825 $this->_update_post_id( $new_id, $post_id );
826 $this->_remove_post_id( $old_id );
827
828 /**
829 * Fires after the ID of a table has been changed.
830 *
831 * @since 1.1.0
832 *
833 * @param string $new_id New ID of the table.
834 * @param string $old_id Old ID of the table.
835 */
836 do_action( 'tablepress_event_changed_table_id', $new_id, $old_id );
837
838 return true;
839 }
840
841 /**
842 * Get an unused table ID (e.g. for a new table).
843 *
844 * @since 1.0.0
845 *
846 * @return string Unused table ID (e.g. for a new table).
847 */
848 protected function _get_new_table_id() {
849 $tables = $this->tables->get();
850 // 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.
851 do {
852 $tables['last_id'] ++;
853 } while ( $this->table_exists( $tables['last_id'] ) );
854 $this->tables->update( $tables );
855 return (string) $tables['last_id'];
856 }
857
858 /**
859 * Get the template for an empty table.
860 *
861 * Important: This scheme is versioned via TablePress::table_scheme_version; changes likely need a version update!
862 *
863 * @since 1.0.0
864 *
865 * @return array Empty table.
866 */
867 public function get_table_template() {
868 // Attention: Array keys have to be lowercase, to make it possible to match them with Shortcode attributes!
869 $table = array(
870 'id' => false,
871 'name' => '',
872 'description' => '',
873 'data' => array( array( '' ) ), // one empty cell
874 // 'created' => current_time( 'mysql' ),
875 'last_modified' => current_time( 'mysql' ),
876 'author' => get_current_user_id(),
877 'options' => array(
878 'last_editor' => get_current_user_id(),
879 'table_head' => true,
880 'table_foot' => false,
881 'alternating_row_colors' => true,
882 'row_hover' => true,
883 'print_name' => false,
884 'print_name_position' => 'above',
885 'print_description' => false,
886 'print_description_position' => 'below',
887 'extra_css_classes' => '',
888 // DataTables JavaScript library
889 'use_datatables' => true,
890 'datatables_sort' => true,
891 'datatables_filter' => true,
892 'datatables_paginate' => true,
893 'datatables_lengthchange' => true,
894 'datatables_paginate_entries' => 10,
895 'datatables_info' => true,
896 'datatables_scrollx' => false,
897 'datatables_custom_commands' => '',
898 ),
899 'visibility' => array(
900 'rows' => array( 1 ), // one visbile row
901 'columns' => array( 1 ), // one visible column
902 ),
903 );
904 /**
905 * Filter the default template/structure of an empty table.
906 *
907 * @since 1.0.0
908 *
909 * @param array $table Default template/structure of an empty table.
910 */
911 return apply_filters( 'tablepress_table_template', $table );
912 }
913
914 /**
915 * Combine two tables (e.g. an existing one with the updated data, or an empty one with new data).
916 *
917 * Performs consistency checks on data and visibility settings.
918 *
919 * @since 1.0.0
920 *
921 * @param array $table Table to merge into.
922 * @param array $new_table Table to merge.
923 * @param bool $table_size_check Optional. Whether to check the number of rows and columns (e.g. not necessary for added or copied tables).
924 * @return array|WP_Error Merged table on success, WP_Error on error.
925 */
926 public function prepare_table( array $table, array $new_table, $table_size_check = true ) {
927 // Table ID must be the same (if there was an ID already).
928 if ( false !== $table['id'] ) {
929 if ( $table['id'] !== $new_table['id'] ) {
930 return new WP_Error( 'table_prepare_no_id_match', '', $new_table['id'] );
931 }
932 }
933
934 // Name, description, and data array need to exist, data must not be empty, the others could be ''.
935 if ( ! isset( $new_table['name'] )
936 || ! isset( $new_table['description'] )
937 || empty( $new_table['data'] )
938 || empty( $new_table['data'][0] ) ) {
939 return new WP_Error( 'table_prepare_name_description_or_data_not_set' );
940 }
941
942 // Visibility needs to exist.
943 if ( ! isset( $new_table['visibility'] )
944 || ! isset( $new_table['visibility']['rows'] )
945 || ! isset( $new_table['visibility']['columns'] ) ) {
946 return new WP_Error( 'table_prepare_visibility_not_set' );
947 }
948 $new_table['visibility']['rows'] = array_map( 'intval', $new_table['visibility']['rows'] );
949 $new_table['visibility']['columns'] = array_map( 'intval', $new_table['visibility']['columns'] );
950
951 // Check dimensions of table data array (not done for newly added, copied, or imported tables).
952 if ( $table_size_check ) {
953 if ( empty( $new_table['number'] )
954 || ! isset( $new_table['number']['rows'] )
955 || ! isset( $new_table['number']['columns'] ) ) {
956 return new WP_Error( 'table_prepare_size_check_numbers_not_set' );
957 }
958 // Table data needs to be ok, and have the correct number of rows and columns.
959 $new_table['number']['rows'] = intval( $new_table['number']['rows'] );
960 $new_table['number']['columns'] = intval( $new_table['number']['columns'] );
961 if ( 0 === $new_table['number']['rows']
962 || 0 === $new_table['number']['columns']
963 || count( $new_table['data'] ) !== $new_table['number']['rows']
964 || count( $new_table['data'][0] ) !== $new_table['number']['columns'] ) {
965 return new WP_Error( 'table_prepare_size_check_numbers_dont_match' );
966 }
967 // Visibility also needs to have correct dimensions.
968 if ( count( $new_table['visibility']['rows'] ) !== $new_table['number']['rows']
969 || count( $new_table['visibility']['columns'] ) !== $new_table['number']['columns'] ) {
970 return new WP_Error( 'table_prepare_size_check_visibility_doesnt_match' );
971 }
972 }
973
974 // All checks were successful, replace original values with new ones.
975
976 // $table['id'] is either false (and remains false) or already equal to $new_table['id'].
977 $table['new_id'] = isset( $new_table['new_id'] ) ? $new_table['new_id'] : $table['id'];
978 $table['name'] = $new_table['name'];
979 $table['description'] = $new_table['description'];
980 $table['data'] = $new_table['data'];
981 // $table['author'] = get_current_user_id(); // We don't want this, as it would override the original author.
982 // $table['created'] = current_time( 'mysql' ); // We don't want this, as it would override the original datetime.
983 $table['last_modified'] = current_time( 'mysql' );
984 $table['options']['last_editor'] = get_current_user_id();
985 // Table Options.
986 if ( isset( $new_table['options'] ) ) { // is for example not set for newly added tables
987 // Specials check for certain options.
988 if ( isset( $new_table['options']['extra_css_classes'] ) ) {
989 $new_table['options']['extra_css_classes'] = explode( ' ', $new_table['options']['extra_css_classes'] );
990 $new_table['options']['extra_css_classes'] = array_map( array( 'TablePress', 'sanitize_css_class' ), $new_table['options']['extra_css_classes'] );
991 $new_table['options']['extra_css_classes'] = array_unique( $new_table['options']['extra_css_classes'] );
992 $new_table['options']['extra_css_classes'] = trim( implode( ' ', $new_table['options']['extra_css_classes'] ) );
993 }
994 if ( isset( $new_table['options']['datatables_paginate_entries'] ) ) {
995 $new_table['options']['datatables_paginate_entries'] = intval( $new_table['options']['datatables_paginate_entries'] );
996 if ( $new_table['options']['datatables_paginate_entries'] < 1 ) {
997 $new_table['options']['datatables_paginate_entries'] = 10; // default value
998 }
999 }
1000 // Merge new options.
1001 $default_table = $this->get_table_template();
1002 $table['options'] = array_intersect_key( $table['options'], $default_table['options'] );
1003 $new_table['options'] = array_intersect_key( $new_table['options'], $default_table['options'] );
1004 $table['options'] = array_merge( $table['options'], $new_table['options'] );
1005 }
1006 // Table Visibility.
1007 $table['visibility']['rows'] = $new_table['visibility']['rows'];
1008 $table['visibility']['columns'] = $new_table['visibility']['columns'];
1009 // Convert DataTables 1.9 parameters (Hungarian notation) to DataTables 1.10 parameters (camelCase notation).
1010 if ( '' !== $table['options']['datatables_custom_commands'] ) {
1011 $table['options']['datatables_custom_commands'] = strtr( $table['options']['datatables_custom_commands'], $this->datatables_parameter_mappings );
1012 }
1013
1014 return $table;
1015 }
1016
1017 /**
1018 * Save the table options of a table (in a post meta field of the table's post).
1019 *
1020 * @since 1.0.0
1021 *
1022 * @param int $post_id Post ID.
1023 * @param array $options Table options.
1024 * @return bool True on success, false on error.
1025 */
1026 protected function _add_table_options( $post_id, array $options ) {
1027 $options = wp_json_encode( $options, TABLEPRESS_JSON_OPTIONS );
1028 return $this->model_post->add_meta_field( $post_id, $this->table_options_field_name, $options );
1029 }
1030
1031 /**
1032 * Update the table options of a table (in a post meta field in the table's post).
1033 *
1034 * @since 1.0.0
1035 *
1036 * @param int $post_id Post ID.
1037 * @param array $options Table options.
1038 * @return bool True on success, false on error.
1039 */
1040 protected function _update_table_options( $post_id, array $options ) {
1041 $options = wp_json_encode( $options, TABLEPRESS_JSON_OPTIONS );
1042 return $this->model_post->update_meta_field( $post_id, $this->table_options_field_name, $options );
1043 }
1044
1045 /**
1046 * Get the table options of a table (from a post meta field of the table's post).
1047 *
1048 * @since 1.0.0
1049 *
1050 * @param int $post_id Post ID.
1051 * @return array Table options on success, empty array on error.
1052 */
1053 protected function _get_table_options( $post_id ) {
1054 $options = $this->model_post->get_meta_field( $post_id, $this->table_options_field_name );
1055 if ( empty( $options ) ) {
1056 return array();
1057 }
1058 return (array) json_decode( $options, true );
1059 }
1060
1061 /**
1062 * Save the table visibility of a table (in a post meta field of the table's post).
1063 *
1064 * @since 1.0.0
1065 *
1066 * @param int $post_id Post ID.
1067 * @param array $visibility Table visibility.
1068 * @return bool True on success, false on error.
1069 */
1070 protected function _add_table_visibility( $post_id, array $visibility ) {
1071 $visibility = wp_json_encode( $visibility, TABLEPRESS_JSON_OPTIONS );
1072 return $this->model_post->add_meta_field( $post_id, $this->table_visibility_field_name, $visibility );
1073 }
1074
1075 /**
1076 * Update the table visibility of a table (in a post meta field in the table's post).
1077 *
1078 * @since 1.0.0
1079 *
1080 * @param int $post_id Post ID.
1081 * @param array $visibility Table visibility.
1082 * @return bool True on success, false on error.
1083 */
1084 protected function _update_table_visibility( $post_id, array $visibility ) {
1085 $visibility = wp_json_encode( $visibility, TABLEPRESS_JSON_OPTIONS );
1086 return $this->model_post->update_meta_field( $post_id, $this->table_visibility_field_name, $visibility );
1087 }
1088
1089 /**
1090 * Get the table visibility of a table (from a post meta field of the table's post).
1091 *
1092 * @since 1.0.0
1093 *
1094 * @param int $post_id Post ID.
1095 * @return array Table visibility on success, empty array on error.
1096 */
1097 protected function _get_table_visibility( $post_id ) {
1098 $visibility = $this->model_post->get_meta_field( $post_id, $this->table_visibility_field_name );
1099 if ( empty( $visibility ) ) {
1100 return array();
1101 }
1102 return json_decode( $visibility, true );
1103 }
1104
1105 /**
1106 * Merge existing Table Options with default Table Options,
1107 * remove (no longer) existing options, after a table scheme change,
1108 * for all tables.
1109 *
1110 * @since 1.0.0
1111 */
1112 public function merge_table_options_defaults() {
1113 $table_post = $this->tables->get( 'table_post' );
1114 if ( empty( $table_post ) ) {
1115 return;
1116 }
1117
1118 // Prime the meta cache with the table options of all tables.
1119 update_meta_cache( 'post', array_values( $table_post ) );
1120
1121 // Get default Table with default Table Options.
1122 $default_table = $this->get_table_template();
1123
1124 // Go through all tables (this loop now uses the WP cache).
1125 foreach ( $table_post as $table_id => $post_id ) {
1126 $table_options = $this->_get_table_options( $post_id );
1127 // Remove old (i.e. no longer existing) Table Options.
1128 $table_options = array_intersect_key( $table_options, $default_table['options'] );
1129 // Merge current into new Table Options.
1130 $table_options = array_merge( $default_table['options'], $table_options );
1131 $this->_update_table_options( $post_id, $table_options );
1132 }
1133 }
1134
1135 /**
1136 * Convert old parameter names to new ones in DataTables "Custom Commands".
1137 * DataTables 1.9 used Hungarian notation, while DataTables 1.10+ (used since TablePress 1.5) uses camelCase notation.
1138 *
1139 * @since 1.5.0
1140 */
1141 public function convert_datatables_parameter_names_tp15() {
1142 $table_post = $this->tables->get( 'table_post' );
1143 if ( empty( $table_post ) ) {
1144 return;
1145 }
1146
1147 // Prime the meta cache with the table options of all tables.
1148 update_meta_cache( 'post', array_values( $table_post ) );
1149
1150 foreach ( $table_post as $table_id => $post_id ) {
1151 $table_options = $this->_get_table_options( $post_id );
1152
1153 // Nothing to do if there are no "Custom Commands".
1154 if ( empty( $table_options['datatables_custom_commands'] ) ) {
1155 continue;
1156 }
1157 // Run search/replace.
1158 $old_custom_commands = $table_options['datatables_custom_commands'];
1159 $table_options['datatables_custom_commands'] = strtr( $table_options['datatables_custom_commands'], $this->datatables_parameter_mappings );
1160 // No need to save (which runs a DB query) if nothing was replaced in the "Custom Commands".
1161 if ( $old_custom_commands === $table_options['datatables_custom_commands'] ) {
1162 continue;
1163 }
1164
1165 $this->_update_table_options( $post_id, $table_options );
1166 }
1167 }
1168
1169 /**
1170 * Invalidate all table output caches, e.g. after a plugin update.
1171 *
1172 * @since 1.0.0
1173 */
1174 public function invalidate_table_output_caches() {
1175 $table_post = $this->tables->get( 'table_post' );
1176 if ( empty( $table_post ) ) {
1177 return;
1178 }
1179
1180 foreach ( $table_post as $table_id => $post_id ) {
1181 $this->invalidate_table_output_cache( $table_id );
1182 }
1183 }
1184
1185 /**
1186 * Add mime type field to existing posts with the TablePress Custom Post Type,
1187 * so that other plugins know that they are not dealing with plain text.
1188 *
1189 * @since 1.5.0
1190 *
1191 * @global wpdb $wpdb WordPress database abstraction object.
1192 */
1193 public function add_mime_type_to_posts() {
1194 global $wpdb;
1195 $wpdb->update( $wpdb->posts, array( 'post_mime_type' => 'application/json' ), array( 'post_type' => $this->model_post->get_post_type() ) );
1196 }
1197
1198 /**
1199 * Add a table ID for a post (table) that was imported through the WP WXR importer to the table ID to post ID map.
1200 *
1201 * @since 1.5.0
1202 *
1203 * @param int $post_id Post ID of the imported post.
1204 * @param int $original_post_ID Original post ID that the post had on the site where it was exported from.
1205 * @param array $postdata Post data that was imported into the database.
1206 * @param array $post Original post data as it was exported.
1207 */
1208 public function add_table_id_on_wp_import( $post_id, $original_post_ID, array $postdata, array $post ) {
1209 // Bail if the post could not be imported or if the post is not a TablePress table.
1210 if ( is_wp_error( $post_id ) || $this->model_post->get_post_type() !== $postdata['post_type'] ) {
1211 return;
1212 }
1213
1214 // Extract the table IDs from the `_tablepress_export_table_id` post meta field.
1215 $table_ids = array();
1216 if ( isset( $post['postmeta'] ) && is_array( $post['postmeta'] ) ) {
1217 foreach ( $post['postmeta'] as $postmeta ) {
1218 if ( '_tablepress_export_table_id' === $postmeta['key'] ) {
1219 $table_ids = $postmeta['value'];
1220 $table_ids = explode( ',', $table_ids );
1221 break;
1222 }
1223 }
1224 }
1225
1226 // Save the post ID for each of the table IDs.
1227 $post_id_saved = false;
1228 foreach ( $table_ids as $table_id ) {
1229 $table_id = preg_replace( '/[^a-zA-Z0-9_-]/', '', $table_id );
1230 if ( '' === $table_id || $this->table_exists( $table_id ) ) {
1231 continue;
1232 }
1233 $this->_update_post_id( $table_id, $post_id );
1234 $post_id_saved = true;
1235 }
1236
1237 // Save the post ID for a new table ID if it could not be saved for any of the imported table IDs.
1238 if ( ! $post_id_saved ) {
1239 $table_id = $this->_get_new_table_id();
1240 $this->_update_post_id( $table_id, $post_id );
1241 }
1242 }
1243
1244 /**
1245 * Remove the `_tablepress_export_table_id` post meta field from the fields that are imported during a WP WXR import.
1246 *
1247 * @since 1.5.0
1248 *
1249 * @param array $postmeta Post meta fields for the post.
1250 * @param int $post_id Post ID.
1251 * @param array $post Post.
1252 * @return array Modified post meta fields.
1253 */
1254 public function prevent_table_id_post_meta_import_on_wp_import( array $postmeta, $post_id, array $post ) {
1255 // Bail if the post is not a TablePress table.
1256 if ( $this->model_post->get_post_type() !== $post['post_type'] ) {
1257 return $postmeta;
1258 }
1259
1260 // Remove the `_tablepress_export_table_id` post meta field from the post meta fields.
1261 foreach ( $postmeta as $index => $meta ) {
1262 if ( '_tablepress_export_table_id' === $meta['key'] ) {
1263 unset( $postmeta[ $index ] );
1264 }
1265 }
1266
1267 return $postmeta;
1268 }
1269
1270 /**
1271 * Add the table IDs for an exported post (table) to the WP WXR export file.
1272 *
1273 * The table IDs for a table are exported in a faked post meta field.
1274 * As there's no action for adding extra data to the WXR export file, we hijack the `wxr_export_skip_postmeta` filter hook.
1275 *
1276 * @since 1.5.0
1277 *
1278 * @param bool $skip Whether to skip the current post meta. Default false.
1279 * @param string $meta_key Current meta key.
1280 * @param stdClass $meta Current meta object.
1281 */
1282 public function add_table_id_to_wp_export( $skip, $meta_key, $meta ) {
1283 // Bail if the exporter doesn't process a TablePress table right now.
1284 if ( $this->table_options_field_name !== $meta_key ) {
1285 return $skip;
1286 }
1287
1288 // Find all table IDs that map to the post ID of the table that is currently being exported.
1289 $table_post = $this->tables->get( 'table_post' );
1290 $table_ids = array_keys( $table_post, (int) $meta->post_id, true );
1291
1292 // Bail if no table IDs are mapped to this post ID.
1293 if ( empty( $table_ids ) ) {
1294 return $skip;
1295 }
1296
1297 // Pretend that there is a `_tablepress_export_table_id` post meta field with the list of table IDs.
1298 $key = '_tablepress_export_table_id';
1299 $value = wxr_cdata( implode( ',', $table_ids ) );
1300
1301 // Hijack the filter and print extra XML code for our faked post meta field.
1302 echo <<<WXR
1303 <wp:postmeta>
1304 <wp:meta_key>{$key}</wp:meta_key>
1305 <wp:meta_value>{$value}</wp:meta_value>
1306 </wp:postmeta>\n
1307 WXR;
1308
1309 return $skip;
1310 }
1311
1312 /**
1313 * Delete the WP_Option of the model.
1314 *
1315 * @since 1.0.0
1316 */
1317 public function destroy() {
1318 $this->tables->delete();
1319 }
1320
1321 } // class TablePress_Table_Model
1322