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