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