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