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