PluginProbe
TablePress – Tables in WordPress made easy / 3.0
TablePress – Tables in WordPress made easy v3.0
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
← All changes | controllers/controller-frontend.php +350 -206 2.0.43.0 View file →
@@ -21,16 +21,38 @@
21 21 */
22 22 class TablePress_Frontend_Controller extends TablePress_Controller {
23 23
24 24 /**
25 + * File name of the admin screens' parent page in the admin menu.
26 + *
27 + * @since 1.0.0
28 + */
29 + public string $parent_page = 'middle';
30 +
31 + /**
32 + * Whether TablePress admin screens are a top-level menu item in the admin menu.
33 + *
34 + * @since 1.0.0
35 + */
36 + public bool $is_top_level_page = false;
37 +
38 + /**
25 39 * List of tables that are shown for the current request.
26 40 *
27 41 * @since 1.0.0
28 - * @var array
42 + * @var array<string, array{count: int, instances: array<string, array<string, mixed>>}>
29 43 */
30 - protected $shown_tables = array();
44 + protected array $shown_tables = array();
31 45
32 46 /**
47 + * List of registered DataTables datetime formats.
48 + *
49 + * @since 3.0.0
50 + * @var string[]
51 + */
52 + protected array $datatables_datetime_formats = array();
53 +
54 + /**
33 55 * Initiate Frontend functionality.
34 56 *
35 57 * @since 1.0.0
36 58 */
@@ -37,20 +59,18 @@
37 59 public function __construct() {
38 60 parent::__construct();
39 61
40 62 /**
41 - * Filters whether the TablePress Default CSS code shall be loaded.
63 + * Filters the admin menu parent page, which is needed for the construction of plugin URLs.
42 64 *
43 65 * @since 1.0.0
44 66 *
45 - * @param bool $use Whether the Default CSS shall be loaded. Default true.
67 + * @param string $parent_page Current admin menu parent page.
46 68 */
47 - if ( apply_filters( 'tablepress_use_default_css', true ) || TablePress::$model_options->get( 'use_custom_css' ) ) {
48 - add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_css' ) );
49 - }
69 + $this->parent_page = apply_filters( 'tablepress_admin_menu_parent_page', TablePress::$model_options->get( 'admin_menu_parent_page' ) );
70 + $this->is_top_level_page = in_array( $this->parent_page, array( 'top', 'middle', 'bottom' ), true );
50 71
51 - // Add DataTables invocation calls.
52 - add_action( 'wp_print_footer_scripts', array( $this, 'add_datatables_calls' ), 11 ); // After inclusion of files.
72 + add_action( 'wp_print_footer_scripts', array( $this, 'add_datatables_calls' ), 9 ); // Priority 9 so that this runs before `_wp_footer_scripts()`.
53 73
54 74 // Register TablePress Shortcodes. Priority 20 is kept for backwards-compatibility purposes.
55 75 add_action( 'init', array( $this, 'init_shortcodes' ), 20 );
56 76
@@ -73,13 +93,20 @@
73 93
74 94 /**
75 95 * Register the tablepress/table block and its dependencies.
76 96 */
77 - register_block_type(
78 - TABLEPRESS_ABSPATH . 'blocks/table/',
97 + if ( function_exists( 'wp_register_block_metadata_collection' ) ) {
98 + // wp_register_block_metadata_collection() is only available since WP 6.7.
99 + wp_register_block_metadata_collection(
100 + TABLEPRESS_ABSPATH . 'blocks',
101 + TABLEPRESS_ABSPATH . 'blocks/blocks-manifest.php',
102 + );
103 + }
104 + register_block_type_from_metadata(
105 + TABLEPRESS_ABSPATH . 'blocks/table/block.json',
79 106 array(
80 107 'render_callback' => array( $this, 'table_block_render_callback' ),
81 - )
108 + ),
82 109 );
83 110 }
84 111
85 112 /**
@@ -86,24 +113,59 @@
86 113 * Register TablePress Shortcodes.
87 114 *
88 115 * @since 1.0.0
89 116 */
90 - public function init_shortcodes() {
117 + public function init_shortcodes(): void {
91 118 add_shortcode( TablePress::$shortcode, array( $this, 'shortcode_table' ) );
92 119 add_shortcode( TablePress::$shortcode_info, array( $this, 'shortcode_table_info' ) );
93 120 }
94 121
95 122 /**
96 - * Enqueue CSS files for default CSS and "Custom CSS" (if desired).
123 + * Enqueues CSS files for TablePress default CSS and "Custom CSS" (if desired).
97 124 *
125 + * This function is only called when a [table /] Shortcode or "TablePress Table" block is evaluated, so that CSS files are only loaded when needed.
126 + *
127 + * If styles have not been printed to the page (in the `<head>`), the TablePress CSS files will be enqueued.
128 + * If styles have already been printed to the page, the TablePress CSS files will be printed right away (likely in the `<body`>).
129 + *
98 130 * @since 1.0.0
99 131 */
100 - public function enqueue_css() {
101 - /** This filter is documented in controllers/controller-frontend.php */
132 + public function enqueue_css(): void {
133 + /*
134 + * Bail early if the function is called from some action hook outside of the normal rendering process.
135 + * These are often used by e.g. SEO plugins that render the content in additional contexts, e.g. to get an excerpt via an output buffer.
136 + * In these cases, we don't want to enqueue the CSS, as it would likely not be printed on the page.
137 + */
138 + if ( doing_action( 'wp_head' ) || doing_action( 'wp_footer' ) ) {
139 + return;
140 + }
141 +
142 + // Prevent repeated execution via a static variable.
143 + static $css_enqueued = false;
144 + if ( $css_enqueued && ! doing_action( 'enqueue_block_assets' ) ) {
145 + return;
146 + }
147 + $css_enqueued = true;
148 +
149 + /**
150 + * Filters whether the TablePress Default CSS code shall be loaded.
151 + *
152 + * @since 1.0.0
153 + *
154 + * @param bool $use Whether the Default CSS shall be loaded. Default true.
155 + */
102 156 $use_default_css = apply_filters( 'tablepress_use_default_css', true );
157 + $use_custom_css = TablePress::$model_options->get( 'use_custom_css' );
158 +
159 + if ( ! $use_default_css && ! $use_custom_css ) {
160 + // Register a placeholder dependency, so that the handle is known for other styles.
161 + wp_register_style( 'tablepress-default', false ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
162 + return;
163 + }
164 +
103 165 $custom_css = TablePress::$model_options->get( 'custom_css' );
104 - $use_custom_css = ( TablePress::$model_options->get( 'use_custom_css' ) && '' !== $custom_css );
105 - $use_custom_css_file = ( $use_custom_css && TablePress::$model_options->get( 'use_custom_css_file' ) );
166 + $use_custom_css = $use_custom_css && '' !== $custom_css;
167 + $use_custom_css_file = $use_custom_css && TablePress::$model_options->get( 'use_custom_css_file' );
106 168 /**
107 169 * Filters the "Custom CSS" version number that is appended to the enqueued CSS files
108 170 *
109 171 * @since 1.0.0
@@ -109,9 +171,9 @@
109 171 * @since 1.0.0
110 172 *
111 173 * @param int $version The "Custom CSS" version.
112 174 */
113 - $custom_css_version = apply_filters( 'tablepress_custom_css_version', TablePress::$model_options->get( 'custom_css_version' ) );
175 + $custom_css_version = (string) apply_filters( 'tablepress_custom_css_version', TablePress::$model_options->get( 'custom_css_version' ) );
114 176
115 177 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
116 178
117 179 // Determine Default CSS URL.
@@ -131,81 +193,71 @@
131 193 if ( $use_custom_css_combined_file ) {
132 194 $custom_css_combined_url = $tablepress_css->get_custom_css_location( 'combined', 'url' );
133 195 // Need to use 'tablepress-default' instead of 'tablepress-combined' to not break existing TablePress Extensions.
134 196 wp_enqueue_style( 'tablepress-default', $custom_css_combined_url, array(), $custom_css_version );
197 + if ( did_action( 'wp_print_styles' ) ) {
198 + wp_print_styles( 'tablepress-default' );
199 + }
200 + return;
201 + }
202 +
203 + if ( $use_default_css ) {
204 + wp_enqueue_style( 'tablepress-default', $default_css_url, array(), TablePress::version );
135 205 } else {
136 - $custom_css_dependencies = array();
137 - if ( $use_default_css ) {
138 - wp_enqueue_style( 'tablepress-default', $default_css_url, array(), TablePress::version );
139 - // Add dependency to make sure that Custom CSS is printed after Default CSS.
140 - $custom_css_dependencies[] = 'tablepress-default';
206 + // Register a placeholder dependency, so that the handle is known for other styles.
207 + wp_register_style( 'tablepress-default', false ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
208 + }
209 +
210 + $use_custom_css_minified_file = ( $use_custom_css_file && ! SCRIPT_DEBUG && $tablepress_css->load_custom_css_from_file( 'minified' ) );
211 + if ( $use_custom_css_minified_file ) {
212 + $custom_css_minified_url = $tablepress_css->get_custom_css_location( 'minified', 'url' );
213 + wp_enqueue_style( 'tablepress-custom', $custom_css_minified_url, array( 'tablepress-default' ), $custom_css_version );
214 + if ( did_action( 'wp_print_styles' ) ) {
215 + wp_print_styles( 'tablepress-custom' );
141 216 }
217 + return;
218 + }
142 219
143 - $use_custom_css_minified_file = ( $use_custom_css_file && ! SCRIPT_DEBUG && $tablepress_css->load_custom_css_from_file( 'minified' ) );
144 - if ( $use_custom_css_minified_file ) {
145 - $custom_css_minified_url = $tablepress_css->get_custom_css_location( 'minified', 'url' );
146 - wp_enqueue_style( 'tablepress-custom', $custom_css_minified_url, $custom_css_dependencies, $custom_css_version );
147 - return;
220 + $use_custom_css_normal_file = ( $use_custom_css_file && $tablepress_css->load_custom_css_from_file( 'normal' ) );
221 + if ( $use_custom_css_normal_file ) {
222 + $custom_css_normal_url = $tablepress_css->get_custom_css_location( 'normal', 'url' );
223 + wp_enqueue_style( 'tablepress-custom', $custom_css_normal_url, array( 'tablepress-default' ), $custom_css_version );
224 + if ( did_action( 'wp_print_styles' ) ) {
225 + wp_print_styles( 'tablepress-custom' );
148 226 }
227 + return;
228 + }
149 229
150 - $use_custom_css_normal_file = ( $use_custom_css_file && $tablepress_css->load_custom_css_from_file( 'normal' ) );
151 - if ( $use_custom_css_normal_file ) {
152 - $custom_css_normal_url = $tablepress_css->get_custom_css_location( 'normal', 'url' );
153 - wp_enqueue_style( 'tablepress-custom', $custom_css_normal_url, $custom_css_dependencies, $custom_css_version );
154 - return;
230 + if ( $use_custom_css ) {
231 + // Get "Custom CSS" from options, try minified Custom CSS first.
232 + $custom_css_minified = TablePress::$model_options->get( 'custom_css_minified' );
233 + if ( ! empty( $custom_css_minified ) ) {
234 + $custom_css = $custom_css_minified;
155 235 }
156 -
157 - if ( $use_custom_css ) {
158 - // Get "Custom CSS" from options, try minified Custom CSS first.
159 - $custom_css_minified = TablePress::$model_options->get( 'custom_css_minified' );
160 - if ( ! empty( $custom_css_minified ) ) {
161 - $custom_css = $custom_css_minified;
236 + /**
237 + * Filters the "Custom CSS" code that is to be loaded as inline CSS.
238 + *
239 + * @since 1.0.0
240 + *
241 + * @param string $custom_css The "Custom CSS" code.
242 + */
243 + $custom_css = apply_filters( 'tablepress_custom_css', $custom_css );
244 + if ( ! empty( $custom_css ) ) {
245 + wp_add_inline_style( 'tablepress-default', $custom_css );
246 + if ( did_action( 'wp_print_styles' ) ) {
247 + wp_print_styles( 'tablepress-default' );
162 248 }
163 - /**
164 - * Filters the "Custom CSS" code that is to be loaded as inline CSS.
165 - *
166 - * @since 1.0.0
167 - *
168 - * @param string $custom_css The "Custom CSS" code.
169 - */
170 - $custom_css = apply_filters( 'tablepress_custom_css', $custom_css );
171 - if ( ! empty( $custom_css ) ) {
172 - // wp_add_inline_style() requires a loaded CSS file, so we have to work around that if "Default CSS" is disabled.
173 - if ( $use_default_css ) {
174 - // Handle of the file to which the <style> shall be appended.
175 - wp_add_inline_style( 'tablepress-default', $custom_css );
176 - } else {
177 - add_action( 'wp_head', array( $this, '_print_custom_css' ), 8 ); // Priority 8 to hook in right after WP_Styles has been processed.
178 - }
179 - }
249 + return;
180 250 }
181 251 }
182 252 }
183 253
184 254 /**
185 - * Print "Custom CSS" to "wp_head" inline.
255 + * Enqueues the DataTables JavaScript library and its dependencies.
186 256 *
187 - * This is necessary if "Default CSS" is off, and saving "Custom CSS" to a file is not possible.
188 - *
189 - * @since 1.0.0
257 + * @since 3.0.0
190 258 */
191 - public function _print_custom_css() {
192 - // Get "Custom CSS" from options, try minified Custom CSS first.
193 - $custom_css = TablePress::$model_options->get( 'custom_css_minified' );
194 - if ( empty( $custom_css ) ) {
195 - $custom_css = TablePress::$model_options->get( 'custom_css' );
196 - }
197 - /** This filter is documented in controllers/controller-frontend.php */
198 - $custom_css = apply_filters( 'tablepress_custom_css', $custom_css );
199 - echo "<style>\n{$custom_css}\n</style>\n";
200 - }
201 -
202 - /**
203 - * Enqueue the DataTables JavaScript library and its dependencies.
204 - *
205 - * @since 1.0.0
206 - */
207 - protected function _enqueue_datatables() {
259 + protected function enqueue_datatables_files(): void {
208 260 $js_file = 'js/jquery.datatables.min.js';
209 261 $js_url = plugins_url( $js_file, TABLEPRESS__FILE__ );
210 262 /**
211 263 * Filters the URL from which the DataTables JavaScript library file is loaded.
@@ -215,9 +267,23 @@
215 267 * @param string $js_url URL of the DataTables JS library file.
216 268 * @param string $js_file Path and file name of the DataTables JS library file.
217 269 */
218 270 $js_url = apply_filters( 'tablepress_datatables_js_url', $js_url, $js_file );
219 - wp_enqueue_script( 'tablepress-datatables', $js_url, array( 'jquery-core' ), TablePress::version, true );
271 +
272 + $dependencies = array( 'jquery-core' );
273 + if ( ! empty( $this->datatables_datetime_formats ) ) {
274 + $dependencies[] = 'moment';
275 + }
276 + /**
277 + * Filters the dependencies for the DataTables JavaScript library.
278 + *
279 + * @since 3.0.0
280 + *
281 + * @param string[] $dependencies The dependencies for the DataTables JS library.
282 + */
283 + $dependencies = apply_filters( 'tablepress_datatables_js_dependencies', $dependencies );
284 +
285 + wp_enqueue_script( 'tablepress-datatables', $js_url, $dependencies, TablePress::version, true );
220 286 }
221 287
222 288 /**
223 289 * Add JS code for invocation of DataTables JS library.
@@ -223,9 +289,9 @@
223 289 * Add JS code for invocation of DataTables JS library.
224 290 *
225 291 * @since 1.0.0
226 292 */
227 - public function add_datatables_calls() {
293 + public function add_datatables_calls(): void {
228 294 // Prevent repeated execution (which would lead to DataTables error messages) via a static variable.
229 295 static $datatables_calls_printed = false;
230 296 if ( $datatables_calls_printed ) {
231 297 return;
@@ -235,8 +301,21 @@
235 301 // There are no tables with activated DataTables on the page that is currently rendered.
236 302 return;
237 303 }
238 304
305 + $this->enqueue_datatables_files();
306 +
307 + /*
308 + * Don't add the DataTables function calls in the scope of the block editor iframe.
309 + * This is necessary for non-block themes, for others, the repeated execution check above is sufficient.
310 + */
311 + if ( function_exists( 'get_current_screen' ) ) {
312 + $current_screen = get_current_screen();
313 + if ( ( $current_screen instanceof WP_Screen ) && $current_screen->is_block_editor() ) {
314 + return;
315 + }
316 + }
317 +
239 318 // Storage for the DataTables language strings.
240 319 $datatables_language = array();
241 320 // Generate the specific JS commands, depending on chosen features on the "Edit" screen and the Shortcode parameters.
242 321 $commands = array();
@@ -241,8 +320,10 @@
241 320 // Generate the specific JS commands, depending on chosen features on the "Edit" screen and the Shortcode parameters.
242 321 $commands = array();
243 322
244 323 foreach ( $this->shown_tables as $table_id => $table_store ) {
324 + $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.
325 +
245 326 if ( empty( $table_store['instances'] ) ) {
246 327 continue;
247 328 }
248 329
@@ -294,16 +375,16 @@
294 375 * or if the filter was used to change the language file, and the language file exists.
295 376 * Otherwise, use an empty en_US placeholder, so that the strings are filterable later.
296 377 */
297 378 if ( ( 'en_US' !== $datatables_locale || $orig_language_file !== $language_file ) && file_exists( $language_file ) ) {
298 - if ( 0 === substr_compare( $language_file, '.php', -4, 4, false ) ) {
379 + if ( str_ends_with( $language_file, '.php' ) ) {
299 380 $datatables_strings = require $language_file;
300 381 if ( ! is_array( $datatables_strings ) ) {
301 382 $datatables_strings = array();
302 383 }
303 - } elseif ( 0 === substr_compare( $language_file, '.json', -5, 5, false ) ) {
384 + } elseif ( str_ends_with( $language_file, '.json' ) ) {
304 385 $datatables_strings = file_get_contents( $language_file );
305 - $datatables_strings = json_decode( $datatables_strings, true );
386 + $datatables_strings = json_decode( $datatables_strings, true ); // @phpstan-ignore argument.type
306 387 // Check if JSON could be decoded.
307 388 if ( is_null( $datatables_strings ) ) {
308 389 $datatables_strings = array();
309 390 }
@@ -322,63 +403,61 @@
322 403 * Filters the language strings for the DataTables JavaScript library's features.
323 404 *
324 405 * @since 2.0.0
325 406 *
326 - * @param array $datatables_strings The language strings for DataTables.
327 - * @param string $datatables_locale Current locale/language for the DataTables JS library.
407 + * @param array<string, mixed> $datatables_strings The language strings for DataTables.
408 + * @param string $datatables_locale Current locale/language for the DataTables JS library.
328 409 */
329 410 $datatables_language[ $datatables_locale ] = apply_filters( 'tablepress_datatables_language_strings', $datatables_strings, $datatables_locale );
330 411 }
331 - $parameters['language'] = '"language":DT_language["' . $datatables_locale . '"]';
412 + $parameters['language'] = "language:DT_language['{$datatables_locale}']";
332 413
333 414 // These parameters need to be added for performance gain or to overwrite unwanted default behavior.
334 415 if ( $js_options['datatables_sort'] ) {
335 416 // No initial sort.
336 - $parameters['order'] = '"order":[]';
417 + $parameters['order'] = 'order:[]';
337 418 // Don't add additional classes, to speed up sorting.
338 - $parameters['orderClasses'] = '"orderClasses":false';
419 + $parameters['orderClasses'] = 'orderClasses:false';
339 420 }
340 421
341 - // Alternating row colors is default, so remove them if not wanted with [].
342 - $parameters['stripeClasses'] = '"stripeClasses":' . ( ( $js_options['alternating_row_colors'] ) ? '["even","odd"]' : '[]' );
343 -
344 422 // The following options are activated by default, so we only need to "false" them if we don't want them, but don't need to "true" them if we do.
345 423 if ( ! $js_options['datatables_sort'] ) {
346 - $parameters['ordering'] = '"ordering":false';
424 + $parameters['ordering'] = 'ordering:false';
347 425 }
348 426 if ( $js_options['datatables_paginate'] ) {
349 - $parameters['pagingType'] = '"pagingType":"simple"';
427 + $parameters['pagingType'] = "pagingType:'simple_numbers'";
350 428 if ( $js_options['datatables_lengthchange'] ) {
351 429 $length_menu = array( 10, 25, 50, 100 );
352 430 if ( ! in_array( $js_options['datatables_paginate_entries'], $length_menu, true ) ) {
353 431 $length_menu[] = $js_options['datatables_paginate_entries'];
354 432 sort( $length_menu, SORT_NUMERIC );
355 - $parameters['lengthMenu'] = '"lengthMenu":[' . implode( ',', $length_menu ) . ']';
433 + $parameters['lengthMenu'] = 'lengthMenu:[' . implode( ',', $length_menu ) . ']';
356 434 }
357 435 } else {
358 - $parameters['lengthChange'] = '"lengthChange":false';
436 + $parameters['lengthChange'] = 'lengthChange:false';
359 437 }
360 438 if ( 10 !== $js_options['datatables_paginate_entries'] ) {
361 - $parameters['pageLength'] = '"pageLength":' . $js_options['datatables_paginate_entries'];
439 + $parameters['pageLength'] = "pageLength:{$js_options['datatables_paginate_entries']}";
362 440 }
363 441 } else {
364 - $parameters['paging'] = '"paging":false';
442 + $parameters['paging'] = 'paging:false';
365 443 }
366 444 if ( ! $js_options['datatables_filter'] ) {
367 - $parameters['searching'] = '"searching":false';
445 + $parameters['searching'] = 'searching:false';
368 446 }
369 447 if ( ! $js_options['datatables_info'] ) {
370 - $parameters['info'] = '"info":false';
448 + $parameters['info'] = 'info:false';
371 449 }
372 450 if ( $js_options['datatables_scrollx'] ) {
373 - $parameters['scrollX'] = '"scrollX":true';
451 + $parameters['scrollX'] = 'scrollX:true';
374 452 }
375 453 if ( false !== $js_options['datatables_scrolly'] ) {
376 - $parameters['scrollY'] = '"scrollY":"' . preg_replace( '#[^0-9a-z.%]#', '', $js_options['datatables_scrolly'] ) . '"';
377 - $parameters['scrollCollapse'] = '"scrollCollapse":true';
454 + $parameters['scrollY'] = 'scrollY:"' . preg_replace( '#[^0-9a-z.%]#', '', $js_options['datatables_scrolly'] ) . '"';
455 + $parameters['scrollCollapse'] = 'scrollCollapse:true';
378 456 }
379 - if ( ! empty( $js_options['datatables_custom_commands'] ) ) {
380 - $parameters['custom_commands'] = $js_options['datatables_custom_commands'];
457 + if ( '' !== $js_options['datatables_custom_commands'] ) {
458 + $parameters['custom_commands'] = trim( $js_options['datatables_custom_commands'] ); // Remove leading and trailing whitespace.
459 + $parameters['custom_commands'] = trim( $parameters['custom_commands'], ',' ); // Remove potentially leading and trailing commas to prevent JS script errors.
381 460 }
382 461
383 462 /**
384 463 * Filters the parameters that are passed to the DataTables JavaScript library.
@@ -384,40 +463,42 @@
384 463 * Filters the parameters that are passed to the DataTables JavaScript library.
385 464 *
386 465 * @since 1.0.0
387 466 *
388 - * @param array $parameters The parameters for the DataTables JS library.
389 - * @param string $table_id The current table ID.
390 - * @param string $html_id The ID of the table HTML element.
391 - * @param array $js_options The options for the JS library.
467 + * @param array<string, mixed> $parameters The parameters for the DataTables JS library.
468 + * @param string $table_id The current table ID.
469 + * @param string $html_id The ID of the table HTML element.
470 + * @param array<string, mixed> $js_options The options for the JS library.
392 471 */
393 472 $parameters = apply_filters( 'tablepress_datatables_parameters', $parameters, $table_id, $html_id, $js_options );
394 473
395 - // If an existing parameter (in the from `"parameter":`) is set in the "Custom Commands", remove its default value.
396 - if ( isset( $parameters['custom_commands'] ) ) {
397 - foreach ( array_keys( $parameters ) as $maybe_overwritten_parameter ) {
398 - if ( false !== strpos( $parameters['custom_commands'], "\"{$maybe_overwritten_parameter}\":" ) ) {
399 - unset( $parameters[ $maybe_overwritten_parameter ] );
400 - }
474 + // If an existing parameter is set as an object key in the "Custom Commands", remove its separate value, to allow for full overrides.
475 + if ( isset( $parameters['custom_commands'] ) && '' !== $parameters['custom_commands'] ) {
476 + $parameters_in_custom_commands = TablePress::extract_keys_from_js_object_string( '{' . $parameters['custom_commands'] . '}' );
477 + foreach ( $parameters_in_custom_commands as $parameter_in_custom_commands ) {
478 + unset( $parameters[ $parameter_in_custom_commands ] );
401 479 }
402 480 }
403 481
482 + $name = substr( $html_id, 11 ); // Remove "tablepress-" from the HTML ID.
483 + $name = "DT_TP['" . str_replace( '-', '_', $name ) . "']";
404 484 $parameters = implode( ',', $parameters );
405 485 $parameters = ( ! empty( $parameters ) ) ? '{' . $parameters . '}' : '';
406 486
407 - $command = "$('#{$html_id}').DataTable({$parameters});";
487 + $command = "{$name} = new DataTable('#{$html_id}',{$parameters});";
408 488 /**
409 489 * Filters the JavaScript command that invokes the DataTables JavaScript library on one table.
410 490 *
411 491 * @since 1.0.0
412 492 *
413 - * @param string $command The JS command for the DataTables JS library.
414 - * @param string $html_id The ID of the table HTML element.
415 - * @param string $parameters The parameters for the DataTables JS library.
416 - * @param string $table_id The current table ID.
417 - * @param array $js_options The options for the JS library.
493 + * @param string $command The JS command for the DataTables JS library.
494 + * @param string $html_id The ID of the table HTML element.
495 + * @param string $parameters The parameters for the DataTables JS library.
496 + * @param string $table_id The current table ID.
497 + * @param array<string, mixed> $js_options The options for the JS library.
498 + * @param string $name The name of the DataTable instance.
418 499 */
419 - $command = apply_filters( 'tablepress_datatables_command', $command, $html_id, $parameters, $table_id, $js_options );
500 + $command = apply_filters( 'tablepress_datatables_command', $command, $html_id, $parameters, $table_id, $js_options, $name );
420 501 if ( ! empty( $command ) ) {
421 502 $commands[] = $command;
422 503 }
423 504 } // foreach table instance
@@ -424,12 +505,40 @@
424 505 } // foreach table ID
425 506
426 507 // DataTables language/translation handling.
427 508 if ( ! empty( $datatables_language ) ) {
428 - $datatables_language = wp_json_encode( $datatables_language, JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT );
429 - $datatables_language = "var DT_language={$datatables_language};\n";
509 + $datatables_language_command = wp_json_encode( $datatables_language, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT );
510 + $datatables_language_command = "var DT_language={$datatables_language_command};\n";
511 + } else {
512 + $datatables_language_command = '';
430 513 }
431 514
515 + // DataTables datetime format string handling.
516 + if ( ! empty( $this->datatables_datetime_formats ) ) {
517 + // Create a command like `DataTable.datetime('MM/DD/YYYY');DataTable.datetime('DD.MM.YYYY');`.
518 + $datatables_datetime_command = implode(
519 + '',
520 + array_map(
521 + static fn( string $datetime_format ): string => "DataTable.datetime('{$datetime_format}');",
522 + $this->datatables_datetime_formats,
523 + )
524 + ) . "\n";
525 + } else {
526 + $datatables_datetime_command = '';
527 + }
528 +
529 + /**
530 + * Filters the JavaScript code for the DataTables JavaScript library that initializes the automatically detected date/time formats via moment.js.
531 + *
532 + * @since 3.0.0
533 + *
534 + * @param string $datatables_datetime_command The JS code for the DataTables JS library that initializes the date/time formats.
535 + * @param string[] $datatables_datetime_formats The date/time formats for moment.js.
536 + */
537 + $datatables_datetime_command = apply_filters( 'tablepress_datatables_datetime_command', $datatables_datetime_command, $this->datatables_datetime_formats );
538 +
539 + $datatables_pre_commands = $datatables_language_command . $datatables_datetime_command;
540 +
432 541 $commands = implode( "\n", $commands );
433 542 /**
434 543 * Filters the JavaScript commands that invoke the DataTables JavaScript library on all tables on the page.
435 544 *
@@ -437,31 +546,30 @@
437 546 *
438 547 * @param string $commands The JS commands for the DataTables JS library.
439 548 */
440 549 $commands = apply_filters( 'tablepress_all_datatables_commands', $commands );
441 - if ( empty( $commands ) ) {
550 + if ( '' === $commands ) {
442 551 return;
443 552 }
444 553
445 - $script_type_attr = current_theme_supports( 'html5', 'script' ) ? '' : ' type="text/javascript"';
446 -
447 - $js_wrapper = <<<'JS'
448 -<script%3$s>
449 -jQuery(function($){
450 -%1$s%2$s
451 -});
452 -</script>
453 -JS;
554 + $script_template = <<<'JS'
555 + var DT_TP = {};
556 + jQuery(($)=>{
557 + %1$s%2$s
558 + });
559 + JS;
454 560 /**
455 561 * Filters the script/jQuery wrapper code for the DataTables commands calls.
456 562 *
457 563 * @since 1.14.0
458 564 *
459 - * @param string $js_wrapper Default script/jQuery wrapper code for the DataTables commands calls.
565 + * @param string $script_template Default script/jQuery wrapper code for the DataTables commands calls.
460 566 */
461 - $js_wrapper = apply_filters( 'tablepress_all_datatables_commands_wrapper', $js_wrapper );
462 - printf( $js_wrapper, $datatables_language, $commands, $script_type_attr );
567 + $script_template = apply_filters( 'tablepress_all_datatables_commands_wrapper', $script_template );
463 568
569 + $script = sprintf( $script_template, $datatables_pre_commands, $commands );
570 + wp_add_inline_script( 'tablepress-datatables', $script );
571 +
464 572 // Prevent repeated execution (which would lead to DataTables error messages) via a static variable.
465 573 $datatables_calls_printed = true;
466 574 }
467 575
@@ -469,15 +577,16 @@
469 577 * Handle Shortcode [table id=<ID> /].
470 578 *
471 579 * @since 1.0.0
472 580 *
473 - * @param array $shortcode_atts List of attributes that where included in the Shortcode.
581 + * @param array<string, mixed>|string $shortcode_atts List of attributes that where included in the Shortcode. An empty string for empty Shortcodes like [table] or [table /].
474 582 * @return string Resulting HTML code for the table with the ID <ID>.
475 583 */
476 - public function shortcode_table( $shortcode_atts ) {
477 - // Don't use `array` type hint in method declaration, as for empty Shortcodes like [table] or [table /], an empty string is passed, see WP Core #26927.
584 + public function shortcode_table( /* array|string */ $shortcode_atts ): string {
478 585 $shortcode_atts = (array) $shortcode_atts;
479 586
587 + $this->enqueue_css();
588 +
480 589 $_render = TablePress::load_class( 'TablePress_Render', 'class-render.php', 'classes' );
481 590
482 591 $default_shortcode_atts = $_render->get_default_render_options();
483 592 /**
@@ -484,9 +593,9 @@
484 593 * Filters the available/default attributes for the [table] Shortcode.
485 594 *
486 595 * @since 1.0.0
487 596 *
488 - * @param array $default_shortcode_atts The [table] Shortcode default attributes.
597 + * @param array<string, mixed> $default_shortcode_atts The [table] Shortcode default attributes.
489 598 */
490 599 $default_shortcode_atts = apply_filters( 'tablepress_shortcode_table_default_shortcode_atts', $default_shortcode_atts );
491 600 // Parse Shortcode attributes, only allow those that are specified.
492 601 $shortcode_atts = shortcode_atts( $default_shortcode_atts, $shortcode_atts ); // Optional third argument left out on purpose. Use filter in the next line instead.
@@ -494,16 +603,16 @@
494 603 * Filters the attributes that were passed to the [table] Shortcode.
495 604 *
496 605 * @since 1.0.0
497 606 *
498 - * @param array $shortcode_atts The attributes passed to the [table] Shortcode.
607 + * @param array<string, mixed> $shortcode_atts The attributes passed to the [table] Shortcode.
499 608 */
500 609 $shortcode_atts = apply_filters( 'tablepress_shortcode_table_shortcode_atts', $shortcode_atts );
501 610
502 611 // Check, if a table with the given ID exists.
503 - $table_id = preg_replace( '/[^a-zA-Z0-9_-]/', '', $shortcode_atts['id'] );
612 + $table_id = (string) preg_replace( '/[^a-zA-Z0-9_-]/', '', $shortcode_atts['id'] );
504 613 if ( ! TablePress::$model_table->table_exists( $table_id ) ) {
505 - $message = "[table &#8220;{$table_id}&#8221; not found /]<br />\n";
614 + $message = "&#91;table “{$table_id}” not found /&#93;<br />\n";
506 615 /**
507 616 * Filters the "Table not found" message.
508 617 *
509 618 * @since 1.0.0
@@ -517,9 +626,9 @@
517 626
518 627 // Load table, with table data, options, and visibility settings.
519 628 $table = TablePress::$model_table->load( $table_id, true, true );
520 629 if ( is_wp_error( $table ) ) {
521 - $message = "[table &#8220;{$table_id}&#8221; could not be loaded /]<br />\n";
630 + $message = "&#91;table “{$table_id}” could not be loaded /&#93;<br />\n";
522 631 /**
523 632 * Filters the "Table could not be loaded" message.
524 633 *
525 634 * @since 1.0.0
@@ -531,9 +640,9 @@
531 640 $message = apply_filters( 'tablepress_table_load_error_message', $message, $table_id, $table );
532 641 return $message;
533 642 }
534 643 if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
535 - $message = "<div>Attention: The internal data of table &#8220;{$table_id}&#8221; is corrupted!</div>";
644 + $message = "<div>Attention: The internal data of table “{$table_id}” is corrupted!</div>";
536 645 /**
537 646 * Filters the "Table data is corrupted" message.
538 647 *
539 648 * @since 1.0.0
@@ -561,20 +670,31 @@
561 670
562 671 // Determine options to use (if set in Shortcode, use those, otherwise use stored options, from the "Edit" screen).
563 672 $render_options = array();
564 673 foreach ( $shortcode_atts as $key => $value ) {
565 - // We have to check this, because strings 'true' or 'false' are not recognized as boolean!
566 - if ( is_string( $value ) && 'true' === strtolower( $value ) ) {
567 - $render_options[ $key ] = true;
568 - } elseif ( is_string( $value ) && 'false' === strtolower( $value ) ) {
569 - $render_options[ $key ] = false;
570 - } elseif ( is_null( $value ) && isset( $table['options'][ $key ] ) ) {
674 + if ( is_null( $value ) && isset( $table['options'][ $key ] ) ) {
675 + // Use the table's stored option value, if the Shortcode parameter was not set.
571 676 $render_options[ $key ] = $table['options'][ $key ];
677 + } elseif ( is_string( $value ) ) {
678 + // Convert strings 'true' or 'false' to boolean, keep others.
679 + $value_lowercase = strtolower( $value );
680 + if ( 'true' === $value_lowercase ) {
681 + $render_options[ $key ] = true;
682 + } elseif ( 'false' === $value_lowercase ) {
683 + $render_options[ $key ] = false;
684 + } else {
685 + $render_options[ $key ] = $value;
686 + }
572 687 } else {
688 + // Keep all other values.
573 689 $render_options[ $key ] = $value;
574 690 }
575 691 }
576 692
693 + // Backward compatibility: Convert boolean or numeric string "table_head" and "table_foot" options to integer.
694 + $render_options['table_head'] = absint( $render_options['table_head'] );
695 + $render_options['table_foot'] = absint( $render_options['table_foot'] );
696 +
577 697 // Generate unique HTML ID, depending on how often this table has already been shown on this page.
578 698 if ( ! isset( $this->shown_tables[ $table_id ] ) ) {
579 699 $this->shown_tables[ $table_id ] = array(
580 700 'count' => 0,
@@ -593,9 +713,9 @@
593 713 * @since 1.0.0
594 714 *
595 715 * @param string $html_id The ID of the table HTML element.
596 716 * @param string $table_id The current table ID.
597 - * @param string $count Number of copies of the table with this table ID on the page.
717 + * @param int $count Number of copies of the table with this table ID on the page.
598 718 */
599 719 $render_options['html_id'] = apply_filters( 'tablepress_html_id', $render_options['html_id'], $table_id, $count );
600 720
601 721 // Generate the "Edit Table" link.
@@ -609,9 +729,9 @@
609 729 *
610 730 * @param bool $show Whether to show the "Edit" link below the table. Default true.
611 731 * @param string $table_id The current table ID.
612 732 */
613 - if ( is_user_logged_in() && apply_filters( 'tablepress_edit_link_below_table', true, $table['id'] ) && current_user_can( 'tablepress_edit_table', $table['id'] ) ) {
733 + if ( is_user_logged_in() && ! $render_options['block_preview'] && apply_filters( 'tablepress_edit_link_below_table', true, $table['id'] ) && current_user_can( 'tablepress_edit_table', $table['id'] ) ) {
614 734 $render_options['edit_table_url'] = TablePress::url( array( 'action' => 'edit', 'table_id' => $table['id'] ) );
615 735 }
616 736
617 737 /**
@@ -620,23 +740,27 @@
620 740 * The render options are determined from the settings on a table's "Edit" screen and the Shortcode parameters.
621 741 *
622 742 * @since 1.0.0
623 743 *
624 - * @param array $render_options The render options for the table.
625 - * @param array $table The current table.
744 + * @param array<string, mixed> $render_options The render options for the table.
745 + * @param array<string, mixed> $table The current table.
626 746 */
627 747 $render_options = apply_filters( 'tablepress_table_render_options', $render_options, $table );
628 748
749 + // Backward compatibility: Convert boolean "table_head" and "table_foot" options to integer, in case they were overwritten via the filter hook.
750 + $render_options['table_head'] = absint( $render_options['table_head'] );
751 + $render_options['table_foot'] = absint( $render_options['table_foot'] );
752 +
629 753 // Check if table output shall and can be loaded from the transient cache, otherwise generate the output.
630 754 if ( $render_options['cache_table_output'] && ! is_user_logged_in() ) {
631 755 // Hash the Render Options array to get a unique cache identifier.
632 - $table_hash = md5( wp_json_encode( $render_options, TABLEPRESS_JSON_OPTIONS ) );
756 + $table_hash = md5( wp_json_encode( $render_options, TABLEPRESS_JSON_OPTIONS ) ); // @phpstan-ignore argument.type
633 757 $transient_name = 'tablepress_' . $table_hash; // Attention: This string must not be longer than 45 characters!
634 758 $output = get_transient( $transient_name );
635 759 if ( false === $output || '' === $output ) {
636 760 // Render/generate the table HTML, as it was not found in the cache.
637 761 $_render->set_input( $table, $render_options );
638 - $output = $_render->get_output();
762 + $output = $_render->get_output( 'html' );
639 763 // Save render output in a transient, set cache timeout to 24 hours.
640 764 set_transient( $transient_name, $output, DAY_IN_SECONDS );
641 765 // Update output caches list transient (necessary for cache invalidation upon table saving).
642 766 $caches_list_transient_name = 'tablepress_c_' . md5( $table_id );
@@ -662,18 +786,16 @@
662 786 }
663 787 } else {
664 788 // Render/generate the table HTML, as no cache is to be used.
665 789 $_render->set_input( $table, $render_options );
666 - $output = $_render->get_output();
790 + $output = $_render->get_output( 'html' );
667 791 }
668 792
669 793 // If DataTables is to be and can be used with this instance of a table, process its parameters and register the call for inclusion in the footer.
670 794 if ( $render_options['use_datatables']
671 - && $render_options['table_head']
672 - && false !== strpos( $output, '<thead' ) // A `<thead>` tag is required.
673 - && false === strpos( $output, ' colspan="' ) // `colspan` attributes are forbidden.
674 - && false === strpos( $output, ' rowspan="' ) // `rowspan` attributes are forbidden.
675 - ) {
795 + && 0 < $render_options['table_head']
796 + && ! str_contains( $output, 'tbody-has-connected-cells' ) // The Render class adds this CSS class to the `<table>` element if the table has connected cells in the `<tbody>`.
797 + ) {
676 798 // Get options for the DataTables JavaScript library from the table's render options.
677 799 $js_options = array();
678 800 foreach ( array(
679 801 'alternating_row_colors',
@@ -698,20 +820,31 @@
698 820 * They are part of the render options and can be overwritten with Shortcode parameters.
699 821 *
700 822 * @since 1.0.0
701 823 *
702 - * @param array $js_options The JavaScript options for the table.
703 - * @param string $table_id The current table ID.
704 - * @param array $render_options The render options for the table.
824 + * @param array<string, mixed> $js_options The JavaScript options for the table.
825 + * @param string $table_id The current table ID.
826 + * @param array<string, mixed> $render_options The render options for the table.
705 827 */
706 828 $js_options = apply_filters( 'tablepress_table_js_options', $js_options, $table_id, $render_options );
707 - $this->shown_tables[ $table_id ]['instances'][ $render_options['html_id'] ] = $js_options;
708 - $this->_enqueue_datatables();
829 +
830 + $this->shown_tables[ $table_id ]['instances'][ (string) $render_options['html_id'] ] = $js_options;
831 +
832 + // DataTables datetime format string handling.
833 + if ( '' !== $render_options['datatables_datetime'] ) {
834 + $render_options['datatables_datetime'] = explode( '|', $render_options['datatables_datetime'] );
835 + foreach ( $render_options['datatables_datetime'] as $datetime_format ) {
836 + $datetime_format = trim( $datetime_format );
837 + if ( '' !== $datetime_format && ! in_array( $datetime_format, $this->datatables_datetime_formats, true ) ) {
838 + $this->datatables_datetime_formats[] = $datetime_format;
839 + }
840 + }
841 + }
709 842 }
710 843
711 844 // Maybe print a list of used render options.
712 845 if ( $render_options['shortcode_debug'] && is_user_logged_in() ) {
713 - $output .= '<pre>' . var_export( $render_options, true ) . '</pre>';
846 + $output .= '<pre>' . var_export( $render_options, true ) . '</pre>'; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
714 847 }
715 848
716 849 return $output;
717 850 }
@@ -720,13 +853,12 @@
720 853 * Handle Shortcode [table-info id=<ID> field=<name> /].
721 854 *
722 855 * @since 1.0.0
723 856 *
724 - * @param array $shortcode_atts List of attributes that where included in the Shortcode.
857 + * @param array<string, mixed>|string $shortcode_atts List of attributes that where included in the Shortcode. An empty string for empty Shortcodes like [table] or [table /].
725 858 * @return string Text that replaces the Shortcode (error message or asked-for information).
726 859 */
727 - public function shortcode_table_info( $shortcode_atts ) {
728 - // Don't use `array` type hint in method declaration, as for empty Shortcodes like [table-info] or [table-info /], an empty string is passed, see WP Core #26927.
860 + public function shortcode_table_info( /* array|string */ $shortcode_atts ): string {
729 861 $shortcode_atts = (array) $shortcode_atts;
730 862
731 863 // Parse Shortcode attributes, only allow those that are specified.
732 864 $default_shortcode_atts = array(
@@ -738,9 +870,9 @@
738 870 * Filters the available/default attributes for the [table-info] Shortcode.
739 871 *
740 872 * @since 1.0.0
741 873 *
742 - * @param array $default_shortcode_atts The [table-info] Shortcode default attributes.
874 + * @param array<string, mixed> $default_shortcode_atts The [table-info] Shortcode default attributes.
743 875 */
744 876 $default_shortcode_atts = apply_filters( 'tablepress_shortcode_table_info_default_shortcode_atts', $default_shortcode_atts );
745 877 $shortcode_atts = shortcode_atts( $default_shortcode_atts, $shortcode_atts ); // Optional third argument left out on purpose. Use filter in the next line instead.
746 878 /**
@@ -747,9 +879,9 @@
747 879 * Filters the attributes that were passed to the [table-info] Shortcode.
748 880 *
749 881 * @since 1.0.0
750 882 *
751 - * @param array $shortcode_atts The attributes passed to the [table-info] Shortcode.
883 + * @param array<string, mixed> $shortcode_atts The attributes passed to the [table-info] Shortcode.
752 884 */
753 885 $shortcode_atts = apply_filters( 'tablepress_shortcode_table_info_shortcode_atts', $shortcode_atts );
754 886
755 887 /**
@@ -756,13 +888,13 @@
756 888 * Filters whether the output of the [table-info] Shortcode is overwritten/short-circuited.
757 889 *
758 890 * @since 1.0.0
759 891 *
760 - * @param bool|string $overwrite Whether the [table-info] output is overwritten. Return false for the regular content, and a string to overwrite the output.
761 - * @param array $shortcode_atts The attributes passed to the [table-info] Shortcode.
892 + * @param false|string $overwrite Whether the [table-info] output is overwritten. Return false for the regular content, and a string to overwrite the output.
893 + * @param array<string, mixed> $shortcode_atts The attributes passed to the [table-info] Shortcode.
762 894 */
763 895 $overwrite = apply_filters( 'tablepress_shortcode_table_info_overwrite', false, $shortcode_atts );
764 - if ( $overwrite ) {
896 + if ( is_string( $overwrite ) ) {
765 897 return $overwrite;
766 898 }
767 899
768 900 // Check, if a table with the given ID exists.
@@ -767,9 +899,9 @@
767 899
768 900 // Check, if a table with the given ID exists.
769 901 $table_id = preg_replace( '/[^a-zA-Z0-9_-]/', '', $shortcode_atts['id'] );
770 902 if ( ! TablePress::$model_table->table_exists( $table_id ) ) {
771 - $message = "[table &#8220;{$table_id}&#8221; not found /]<br />\n";
903 + $message = "&#91;table “{$table_id}” not found /&#93;<br />\n";
772 904 /** This filter is documented in controllers/controller-frontend.php */
773 905 $message = apply_filters( 'tablepress_table_not_found_message', $message, $table_id );
774 906 return $message;
775 907 }
@@ -776,16 +908,16 @@
776 908
777 909 // Load table, with table data, options, and visibility settings.
778 910 $table = TablePress::$model_table->load( $table_id, true, true );
779 911 if ( is_wp_error( $table ) ) {
780 - $message = "[table &#8220;{$table_id}&#8221; could not be loaded /]<br />\n";
912 + $message = "&#91;table “{$table_id}” could not be loaded /&#93;<br />\n";
781 913 /** This filter is documented in controllers/controller-frontend.php */
782 914 $message = apply_filters( 'tablepress_table_load_error_message', $message, $table_id, $table );
783 915 return $message;
784 916 }
785 917
786 - $field = preg_replace( '/[^a-z_]/', '', strtolower( $shortcode_atts['field'] ) );
787 - $format = preg_replace( '/[^a-z]/', '', strtolower( $shortcode_atts['format'] ) );
918 + $field = (string) preg_replace( '/[^a-z_]/', '', strtolower( $shortcode_atts['field'] ) );
919 + $format = (string) preg_replace( '/[^a-z]/', '', strtolower( $shortcode_atts['format'] ) );
788 920
789 921 // Generate output, depending on what information (field) was asked for.
790 922 switch ( $field ) {
791 923 case 'name':
@@ -799,9 +931,13 @@
799 931 $output = $table['last_modified'];
800 932 break;
801 933 case 'human':
802 934 $modified_timestamp = date_create( $table['last_modified'], wp_timezone() );
803 - $modified_timestamp = $modified_timestamp->getTimestamp();
935 + if ( false === $modified_timestamp ) {
936 + $modified_timestamp = $table['last_modified'];
937 + } else {
938 + $modified_timestamp = $modified_timestamp->getTimestamp();
939 + }
804 940 $current_timestamp = time();
805 941 $time_diff = $current_timestamp - $modified_timestamp;
806 942 // Time difference is only shown up to one week.
807 943 if ( $time_diff >= 0 && $time_diff < WEEK_IN_SECONDS ) {
@@ -829,14 +965,10 @@
829 965 break;
830 966 case 'number_rows':
831 967 $output = count( $table['data'] );
832 968 if ( 'raw' !== $format ) {
833 - if ( $table['options']['table_head'] ) {
834 - $output = $output - 1;
835 - }
836 - if ( $table['options']['table_foot'] ) {
837 - $output = $output - 1;
838 - }
969 + $output -= $table['options']['table_head'];
970 + $output -= $table['options']['table_foot'];
839 971 }
840 972 break;
841 973 case 'number_columns':
842 974 $output = count( $table['data'][0] );
@@ -841,18 +973,18 @@
841 973 case 'number_columns':
842 974 $output = count( $table['data'][0] );
843 975 break;
844 976 default:
845 - $output = "[table-info field &#8220;{$field}&#8221; not found in table &#8220;{$table_id}&#8221; /]<br />\n";
977 + $output = "&#91;table-info field “{$field}” not found in table “{$table_id}” /&#93;<br />\n";
846 978 /**
847 979 * Filters the "table info field not found" message.
848 980 *
849 981 * @since 1.0.0
850 982 *
851 - * @param string $output The "table info field not found" message.
852 - * @param array $table The current table ID.
853 - * @param string $field The field that was not found.
854 - * @param string $format The return format for the field.
983 + * @param string $output The "table info field not found" message.
984 + * @param array<string, mixed> $table The current table.
985 + * @param string $field The field that was not found.
986 + * @param string $format The return format for the field.
855 987 */
856 988 $output = apply_filters( 'tablepress_table_info_not_found_message', $output, $table, $field, $format );
857 989 }
858 990
@@ -860,11 +992,11 @@
860 992 * Filters the output of the [table-info] Shortcode.
861 993 *
862 994 * @since 1.0.0
863 995 *
864 - * @param string $output The output of the [table-info] Shortcode.
865 - * @param array $table The current table.
866 - * @param array $shortcode_atts The attributes passed to the [table-info] Shortcode.
996 + * @param string $output The output of the [table-info] Shortcode.
997 + * @param array<string, mixed> $table The current table.
998 + * @param array<string, mixed> $shortcode_atts The attributes passed to the [table-info] Shortcode.
867 999 */
868 1000 $output = apply_filters( 'tablepress_shortcode_table_info_output', $output, $table, $shortcode_atts );
869 1001 return $output;
870 1002 }
@@ -882,11 +1014,18 @@
882 1014 *
883 1015 * @param string $search_sql Current part of the "WHERE" clause of the SQL statement used to get posts/pages from the WP database that is related to searching.
884 1016 * @return string Eventually extended SQL "WHERE" clause, to also find posts/pages with Shortcodes in them.
885 1017 */
886 - public function posts_search_filter( $search_sql ) {
1018 + public function posts_search_filter( /* string */ $search_sql ): string {
1019 + // Don't use a type hint in the method declaration as there can be cases where `null` is passed to the filter hook callback somehow.
1020 +
887 1021 global $wpdb;
888 1022
1023 + // Protect against cases where `null` is somehow passed to the filter hook callback.
1024 + if ( ! is_string( $search_sql ) ) { // @phpstan-ignore function.alreadyNarrowedType (The `is_string()` check is needed as the input is coming from a filter hook.)
1025 + return '';
1026 + }
1027 +
889 1028 if ( ! is_search() || ! is_main_query() ) {
890 1029 return $search_sql;
891 1030 }
892 1031
@@ -904,16 +1043,21 @@
904 1043 foreach ( $table_ids as $table_id ) {
905 1044 // Load table, with table data, options, and visibility settings.
906 1045 $table = TablePress::$model_table->load( $table_id, true, true );
907 1046
1047 + // Skip tables that could not be loaded.
1048 + if ( is_wp_error( $table ) ) {
1049 + continue;
1050 + }
1051 +
1052 + // Do not search in corrupted tables.
908 1053 if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
909 - // Do not search in corrupted tables.
910 1054 continue;
911 1055 }
912 1056
913 1057 foreach ( $search_terms as $search_term ) {
914 - if ( ( $table['options']['print_name'] && false !== stripos( $table['name'], $search_term ) )
915 - || ( $table['options']['print_description'] && false !== stripos( $table['description'], $search_term ) ) ) {
1058 + if ( ( $table['options']['print_name'] && false !== stripos( $table['name'], (string) $search_term ) )
1059 + || ( $table['options']['print_description'] && false !== stripos( $table['description'], (string) $search_term ) ) ) {
916 1060 // Found the search term in the name or description (and they are shown).
917 1061 $query_result[ $search_term ][] = $table_id; // Add table ID to result list.
918 1062 // No need to continue searching this search term in this table.
919 1063 continue;
@@ -929,10 +1073,10 @@
929 1073 if ( 0 === $table['visibility']['columns'][ $col_idx ] ) {
930 1074 // Column is hidden, so don't search in it.
931 1075 continue;
932 1076 }
933 - // @TODO: Cells are not evaluated here, so math formulas are searched.
934 - if ( false !== stripos( $table_cell, $search_term ) ) {
1077 + // @todo Cells are not evaluated here, so math formulas are searched.
1078 + if ( false !== stripos( $table_cell, (string) $search_term ) ) {
935 1079 // Found the search term in the cell content.
936 1080 $query_result[ $search_term ][] = $table_id; // Add table ID to result list
937 1081 // No need to continue searching this search term in this table.
938 1082 continue 3;
@@ -949,9 +1093,9 @@
949 1093 $n = ( empty( $exact ) ) ? '%' : '';
950 1094 $search_sql = $wpdb->remove_placeholder_escape( $search_sql );
951 1095 foreach ( $query_result as $search_term => $table_ids ) {
952 1096 $search_term = esc_sql( $wpdb->esc_like( $search_term ) );
953 - $old_or = "OR ({$wpdb->posts}.post_content LIKE '{$n}{$search_term}{$n}')";
1097 + $old_or = "OR ({$wpdb->posts}.post_content LIKE '{$n}{$search_term}{$n}')"; // @phpstan-ignore encapsedStringPart.nonString (The esc_sql() call above returns a string, as a string is passed.)
954 1098 $table_ids = implode( '|', $table_ids );
955 1099 $regexp = '\\\\[' . TablePress::$shortcode . ' id=(["\\\']?)(' . $table_ids . ')([\]"\\\' /])'; // ' needs to be single escaped, [ double escaped (with \\) in mySQL
956 1100 $new_or = $old_or . " OR ({$wpdb->posts}.post_content REGEXP '{$regexp}')";
957 1101 $search_sql = str_replace( $old_or, $new_or, $search_sql );
@@ -965,15 +1109,15 @@
965 1109 * Callback function for rendering the tablepress/table block.
966 1110 *
967 1111 * @since 2.0.0
968 1112 *
969 - * @param array $block_attributes List of attributes that where included in the block settings.
1113 + * @param array<string, string> $block_attributes List of attributes that where included in the block settings.
970 1114 * @return string Resulting HTML code for the table.
971 1115 */
972 - public function table_block_render_callback( array $block_attributes ) {
1116 + public function table_block_render_callback( array $block_attributes ): string {
973 1117 // Don't return anything if no table was selected.
974 1118 if ( '' === $block_attributes['id'] ) {
975 - return;
1119 + return '';
976 1120 }
977 1121
978 1122 if ( '' !== trim( $block_attributes['parameters'] ) ) {
979 1123 $render_attributes = shortcode_parse_atts( $block_attributes['parameters'] );