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