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