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
tablepress / controllers / controller-frontend.php

controller-frontend.php in TablePress – Tables in WordPress made easy 3.0, at controllers/controller-frontend.php

1,133 lines 45.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Frontend Controller for TablePress with functionality for the frontend
4 *
5 * @package TablePress
6 * @subpackage Controllers
7 * @author Tobias Bäthge
8 * @since 1.0.0
9 */
10
11 // Prohibit direct script loading.
12 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
13
14 /**
15 * Frontend Controller class, extends Base Controller Class
16 *
17 * @package TablePress
18 * @subpackage Controllers
19 * @author Tobias Bäthge
20 * @since 1.0.0
21 */
22 class TablePress_Frontend_Controller extends TablePress_Controller {
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 /**
39 * List of tables that are shown for the current request.
40 *
41 * @since 1.0.0
42 * @var array<string, array{count: int, instances: array<string, array<string, mixed>>}>
43 */
44 protected array $shown_tables = array();
45
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 /**
55 * Initiate Frontend functionality.
56 *
57 * @since 1.0.0
58 */
59 public function __construct() {
60 parent::__construct();
61
62 /**
63 * Filters the admin menu parent page, which is needed for the construction of plugin URLs.
64 *
65 * @since 1.0.0
66 *
67 * @param string $parent_page Current admin menu parent page.
68 */
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 );
71
72 add_action( 'wp_print_footer_scripts', array( $this, 'add_datatables_calls' ), 9 ); // Priority 9 so that this runs before `_wp_footer_scripts()`.
73
74 // Register TablePress Shortcodes. Priority 20 is kept for backwards-compatibility purposes.
75 add_action( 'init', array( $this, 'init_shortcodes' ), 20 );
76
77 /**
78 * Filters whether the WordPress search shall also search TablePress tables.
79 *
80 * @since 1.0.0
81 *
82 * @param bool $search Whether the TablePress tables shall be searched. Default true.
83 */
84 if ( apply_filters( 'tablepress_wp_search_integration', true ) ) {
85 // Extend WordPress Search to also find posts/pages that have a table with the one of the search terms in title (if shown), description (if shown), or content.
86 add_filter( 'posts_search', array( $this, 'posts_search_filter' ) );
87 }
88
89 /**
90 * Load TablePress Template Tag functions.
91 */
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 );
110 }
111
112 /**
113 * Register TablePress Shortcodes.
114 *
115 * @since 1.0.0
116 */
117 public function init_shortcodes(): void {
118 add_shortcode( TablePress::$shortcode, array( $this, 'shortcode_table' ) );
119 add_shortcode( TablePress::$shortcode_info, array( $this, 'shortcode_table_info' ) );
120 }
121
122 /**
123 * Enqueues CSS files for TablePress default CSS and "Custom CSS" (if desired).
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 *
130 * @since 1.0.0
131 */
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 */
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
165 $custom_css = TablePress::$model_options->get( 'custom_css' );
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' );
168 /**
169 * Filters the "Custom CSS" version number that is appended to the enqueued CSS files
170 *
171 * @since 1.0.0
172 *
173 * @param int $version The "Custom CSS" version.
174 */
175 $custom_css_version = (string) apply_filters( 'tablepress_custom_css_version', TablePress::$model_options->get( 'custom_css_version' ) );
176
177 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
178
179 // Determine Default CSS URL.
180 $rtl = ( is_rtl() ) ? '-rtl' : '';
181 $unfiltered_default_css_url = plugins_url( "css/build/default{$rtl}.css", TABLEPRESS__FILE__ );
182 /**
183 * Filters the URL from which the TablePress Default CSS file is loaded.
184 *
185 * @since 1.0.0
186 *
187 * @param string $unfiltered_default_css_url URL of the TablePress Default CSS file.
188 */
189 $default_css_url = apply_filters( 'tablepress_default_css_url', $unfiltered_default_css_url );
190
191 $use_custom_css_combined_file = ( $use_default_css && $use_custom_css_file && ! SCRIPT_DEBUG && ! is_rtl() && $unfiltered_default_css_url === $default_css_url && $tablepress_css->load_custom_css_from_file( 'combined' ) );
192
193 if ( $use_custom_css_combined_file ) {
194 $custom_css_combined_url = $tablepress_css->get_custom_css_location( 'combined', 'url' );
195 // Need to use 'tablepress-default' instead of 'tablepress-combined' to not break existing TablePress Extensions.
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 );
205 } else {
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' );
216 }
217 return;
218 }
219
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' );
226 }
227 return;
228 }
229
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;
235 }
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' );
248 }
249 return;
250 }
251 }
252 }
253
254 /**
255 * Enqueues the DataTables JavaScript library and its dependencies.
256 *
257 * @since 3.0.0
258 */
259 protected function enqueue_datatables_files(): void {
260 $js_file = 'js/jquery.datatables.min.js';
261 $js_url = plugins_url( $js_file, TABLEPRESS__FILE__ );
262 /**
263 * Filters the URL from which the DataTables JavaScript library file is loaded.
264 *
265 * @since 1.0.0
266 *
267 * @param string $js_url URL of the DataTables JS library file.
268 * @param string $js_file Path and file name of the DataTables JS library file.
269 */
270 $js_url = apply_filters( 'tablepress_datatables_js_url', $js_url, $js_file );
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 );
286 }
287
288 /**
289 * Add JS code for invocation of DataTables JS library.
290 *
291 * @since 1.0.0
292 */
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
300 if ( empty( $this->shown_tables ) ) {
301 // There are no tables with activated DataTables on the page that is currently rendered.
302 return;
303 }
304
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();
320 // Generate the specific JS commands, depending on chosen features on the "Edit" screen and the Shortcode parameters.
321 $commands = array();
322
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
326 if ( empty( $table_store['instances'] ) ) {
327 continue;
328 }
329
330 foreach ( $table_store['instances'] as $html_id => $js_options ) {
331 $parameters = array();
332
333 // Settle dependencies/conflicts between certain features.
334 if ( false !== $js_options['datatables_scrolly'] ) { // datatables_scrolly can be a string, so that the explicit `false` check is needed.
335 // Vertical scrolling and pagination don't work together.
336 $js_options['datatables_paginate'] = false;
337 }
338 // Sanitize, as it may come from a Shortcode attribute.
339 $js_options['datatables_paginate_entries'] = (int) $js_options['datatables_paginate_entries'];
340
341 /*
342 * DataTables language/translation handling.
343 */
344
345 /**
346 * Filters the locale/language for the DataTables JavaScript library.
347 *
348 * @since 1.0.0
349 *
350 * @param string $locale The DataTables JS library locale.
351 * @param string $table_id The current table ID.
352 */
353 $datatables_locale = apply_filters( 'tablepress_datatables_locale', $js_options['datatables_locale'], $table_id );
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
359 /**
360 * Filters the language file path for the DataTables JavaScript library.
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 *
365 * @since 1.0.0
366 *
367 * @param string $orig_language_file Language file path for the DataTables JS library.
368 * @param string $datatables_locale Current locale/language for the DataTables JS library.
369 * @param string $tablepress_abspath Base path of the TablePress plugin.
370 */
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();
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 );
411 }
412 $parameters['language'] = "language:DT_language['{$datatables_locale}']";
413
414 // These parameters need to be added for performance gain or to overwrite unwanted default behavior.
415 if ( $js_options['datatables_sort'] ) {
416 // No initial sort.
417 $parameters['order'] = 'order:[]';
418 // Don't add additional classes, to speed up sorting.
419 $parameters['orderClasses'] = 'orderClasses:false';
420 }
421
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.
423 if ( ! $js_options['datatables_sort'] ) {
424 $parameters['ordering'] = 'ordering:false';
425 }
426 if ( $js_options['datatables_paginate'] ) {
427 $parameters['pagingType'] = "pagingType:'simple_numbers'";
428 if ( $js_options['datatables_lengthchange'] ) {
429 $length_menu = array( 10, 25, 50, 100 );
430 if ( ! in_array( $js_options['datatables_paginate_entries'], $length_menu, true ) ) {
431 $length_menu[] = $js_options['datatables_paginate_entries'];
432 sort( $length_menu, SORT_NUMERIC );
433 $parameters['lengthMenu'] = 'lengthMenu:[' . implode( ',', $length_menu ) . ']';
434 }
435 } else {
436 $parameters['lengthChange'] = 'lengthChange:false';
437 }
438 if ( 10 !== $js_options['datatables_paginate_entries'] ) {
439 $parameters['pageLength'] = "pageLength:{$js_options['datatables_paginate_entries']}";
440 }
441 } else {
442 $parameters['paging'] = 'paging:false';
443 }
444 if ( ! $js_options['datatables_filter'] ) {
445 $parameters['searching'] = 'searching:false';
446 }
447 if ( ! $js_options['datatables_info'] ) {
448 $parameters['info'] = 'info:false';
449 }
450 if ( $js_options['datatables_scrollx'] ) {
451 $parameters['scrollX'] = 'scrollX:true';
452 }
453 if ( false !== $js_options['datatables_scrolly'] ) {
454 $parameters['scrollY'] = 'scrollY:"' . preg_replace( '#[^0-9a-z.%]#', '', $js_options['datatables_scrolly'] ) . '"';
455 $parameters['scrollCollapse'] = 'scrollCollapse:true';
456 }
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.
460 }
461
462 /**
463 * Filters the parameters that are passed to the DataTables JavaScript library.
464 *
465 * @since 1.0.0
466 *
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.
471 */
472 $parameters = apply_filters( 'tablepress_datatables_parameters', $parameters, $table_id, $html_id, $js_options );
473
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 ] );
479 }
480 }
481
482 $name = substr( $html_id, 11 ); // Remove "tablepress-" from the HTML ID.
483 $name = "DT_TP['" . str_replace( '-', '_', $name ) . "']";
484 $parameters = implode( ',', $parameters );
485 $parameters = ( ! empty( $parameters ) ) ? '{' . $parameters . '}' : '';
486
487 $command = "{$name} = new DataTable('#{$html_id}',{$parameters});";
488 /**
489 * Filters the JavaScript command that invokes the DataTables JavaScript library on one table.
490 *
491 * @since 1.0.0
492 *
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.
499 */
500 $command = apply_filters( 'tablepress_datatables_command', $command, $html_id, $parameters, $table_id, $js_options, $name );
501 if ( ! empty( $command ) ) {
502 $commands[] = $command;
503 }
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 = '';
513 }
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
541 $commands = implode( "\n", $commands );
542 /**
543 * Filters the JavaScript commands that invoke the DataTables JavaScript library on all tables on the page.
544 *
545 * @since 1.0.0
546 *
547 * @param string $commands The JS commands for the DataTables JS library.
548 */
549 $commands = apply_filters( 'tablepress_all_datatables_commands', $commands );
550 if ( '' === $commands ) {
551 return;
552 }
553
554 $script_template = <<<'JS'
555 var DT_TP = {};
556 jQuery(($)=>{
557 %1$s%2$s
558 });
559 JS;
560 /**
561 * Filters the script/jQuery wrapper code for the DataTables commands calls.
562 *
563 * @since 1.14.0
564 *
565 * @param string $script_template Default script/jQuery wrapper code for the DataTables commands calls.
566 */
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;
574 }
575
576 /**
577 * Handle Shortcode [table id=<ID> /].
578 *
579 * @since 1.0.0
580 *
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 /].
582 * @return string Resulting HTML code for the table with the ID <ID>.
583 */
584 public function shortcode_table( /* array|string */ $shortcode_atts ): string {
585 $shortcode_atts = (array) $shortcode_atts;
586
587 $this->enqueue_css();
588
589 $_render = TablePress::load_class( 'TablePress_Render', 'class-render.php', 'classes' );
590
591 $default_shortcode_atts = $_render->get_default_render_options();
592 /**
593 * Filters the available/default attributes for the [table] Shortcode.
594 *
595 * @since 1.0.0
596 *
597 * @param array<string, mixed> $default_shortcode_atts The [table] Shortcode default attributes.
598 */
599 $default_shortcode_atts = apply_filters( 'tablepress_shortcode_table_default_shortcode_atts', $default_shortcode_atts );
600 // Parse Shortcode attributes, only allow those that are specified.
601 $shortcode_atts = shortcode_atts( $default_shortcode_atts, $shortcode_atts ); // Optional third argument left out on purpose. Use filter in the next line instead.
602 /**
603 * Filters the attributes that were passed to the [table] Shortcode.
604 *
605 * @since 1.0.0
606 *
607 * @param array<string, mixed> $shortcode_atts The attributes passed to the [table] Shortcode.
608 */
609 $shortcode_atts = apply_filters( 'tablepress_shortcode_table_shortcode_atts', $shortcode_atts );
610
611 // Check, if a table with the given ID exists.
612 $table_id = (string) preg_replace( '/[^a-zA-Z0-9_-]/', '', $shortcode_atts['id'] );
613 if ( ! TablePress::$model_table->table_exists( $table_id ) ) {
614 $message = "&#91;table “{$table_id}” not found /&#93;<br />\n";
615 /**
616 * Filters the "Table not found" message.
617 *
618 * @since 1.0.0
619 *
620 * @param string $message The "Table not found" message.
621 * @param string $table_id The current table ID.
622 */
623 $message = apply_filters( 'tablepress_table_not_found_message', $message, $table_id );
624 return $message;
625 }
626
627 // Load table, with table data, options, and visibility settings.
628 $table = TablePress::$model_table->load( $table_id, true, true );
629 if ( is_wp_error( $table ) ) {
630 $message = "&#91;table “{$table_id}” could not be loaded /&#93;<br />\n";
631 /**
632 * Filters the "Table could not be loaded" message.
633 *
634 * @since 1.0.0
635 *
636 * @param string $message The "Table could not be loaded" message.
637 * @param string $table_id The current table ID.
638 * @param WP_Error $table The error object for the table.
639 */
640 $message = apply_filters( 'tablepress_table_load_error_message', $message, $table_id, $table );
641 return $message;
642 }
643 if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
644 $message = "<div>Attention: The internal data of table “{$table_id}” is corrupted!</div>";
645 /**
646 * Filters the "Table data is corrupted" message.
647 *
648 * @since 1.0.0
649 *
650 * @param string $message The "Table data is corrupted" message.
651 * @param string $table_id The current table ID.
652 * @param string $json_error The JSON error with information about the corrupted table.
653 */
654 $message = apply_filters( 'tablepress_table_corrupted_message', $message, $table_id, $table['json_error'] );
655 return $message;
656 }
657
658 /**
659 * Filters whether the "datatables_custom_commands" Shortcode parameter is disabled.
660 *
661 * By default, the "datatables_custom_commands" Shortcode parameter is disabled for security reasons.
662 *
663 * @since 1.0.0
664 *
665 * @param bool $disable Whether to disable the "datatables_custom_commands" Shortcode parameter. Default true.
666 */
667 if ( ! is_null( $shortcode_atts['datatables_custom_commands'] ) && apply_filters( 'tablepress_disable_custom_commands_shortcode_parameter', true ) ) {
668 $shortcode_atts['datatables_custom_commands'] = null;
669 }
670
671 // Determine options to use (if set in Shortcode, use those, otherwise use stored options, from the "Edit" screen).
672 $render_options = array();
673 foreach ( $shortcode_atts as $key => $value ) {
674 if ( is_null( $value ) && isset( $table['options'][ $key ] ) ) {
675 // Use the table's stored option value, if the Shortcode parameter was not set.
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 }
687 } else {
688 // Keep all other values.
689 $render_options[ $key ] = $value;
690 }
691 }
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
697 // Generate unique HTML ID, depending on how often this table has already been shown on this page.
698 if ( ! isset( $this->shown_tables[ $table_id ] ) ) {
699 $this->shown_tables[ $table_id ] = array(
700 'count' => 0,
701 'instances' => array(),
702 );
703 }
704 ++$this->shown_tables[ $table_id ]['count'];
705 $count = $this->shown_tables[ $table_id ]['count'];
706 $render_options['html_id'] = "tablepress-{$table_id}";
707 if ( $count > 1 ) {
708 $render_options['html_id'] .= "-no-{$count}";
709 }
710 /**
711 * Filters the ID of the table HTML element.
712 *
713 * @since 1.0.0
714 *
715 * @param string $html_id The ID of the table HTML element.
716 * @param string $table_id The current table ID.
717 * @param int $count Number of copies of the table with this table ID on the page.
718 */
719 $render_options['html_id'] = apply_filters( 'tablepress_html_id', $render_options['html_id'], $table_id, $count );
720
721 // Generate the "Edit Table" link.
722 $render_options['edit_table_url'] = '';
723 /**
724 * Filters whether the "Edit" link below the table shall be shown.
725 *
726 * The "Edit" link is only shown to logged-in users who possess the necessary capability to edit the table.
727 *
728 * @since 1.0.0
729 *
730 * @param bool $show Whether to show the "Edit" link below the table. Default true.
731 * @param string $table_id The current table ID.
732 */
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'] ) ) {
734 $render_options['edit_table_url'] = TablePress::url( array( 'action' => 'edit', 'table_id' => $table['id'] ) );
735 }
736
737 /**
738 * Filters the render options for the table.
739 *
740 * The render options are determined from the settings on a table's "Edit" screen and the Shortcode parameters.
741 *
742 * @since 1.0.0
743 *
744 * @param array<string, mixed> $render_options The render options for the table.
745 * @param array<string, mixed> $table The current table.
746 */
747 $render_options = apply_filters( 'tablepress_table_render_options', $render_options, $table );
748
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'] );
752
753 // Check if table output shall and can be loaded from the transient cache, otherwise generate the output.
754 if ( $render_options['cache_table_output'] && ! is_user_logged_in() ) {
755 // Hash the Render Options array to get a unique cache identifier.
756 $table_hash = md5( wp_json_encode( $render_options, TABLEPRESS_JSON_OPTIONS ) ); // @phpstan-ignore argument.type
757 $transient_name = 'tablepress_' . $table_hash; // Attention: This string must not be longer than 45 characters!
758 $output = get_transient( $transient_name );
759 if ( false === $output || '' === $output ) {
760 // Render/generate the table HTML, as it was not found in the cache.
761 $_render->set_input( $table, $render_options );
762 $output = $_render->get_output( 'html' );
763 // Save render output in a transient, set cache timeout to 24 hours.
764 set_transient( $transient_name, $output, DAY_IN_SECONDS );
765 // Update output caches list transient (necessary for cache invalidation upon table saving).
766 $caches_list_transient_name = 'tablepress_c_' . md5( $table_id );
767 $caches_list = get_transient( $caches_list_transient_name );
768 if ( false === $caches_list ) {
769 $caches_list = array();
770 } else {
771 $caches_list = (array) json_decode( $caches_list, true );
772 }
773 if ( ! in_array( $transient_name, $caches_list, true ) ) {
774 $caches_list[] = $transient_name;
775 }
776 set_transient( $caches_list_transient_name, wp_json_encode( $caches_list, TABLEPRESS_JSON_OPTIONS ), 2 * DAY_IN_SECONDS );
777 } else {
778 /**
779 * Filters the cache hit comment message.
780 *
781 * @since 1.0.0
782 *
783 * @param string $comment The cache hit comment message.
784 */
785 $output .= apply_filters( 'tablepress_cache_hit_comment', "<!-- #{$render_options['html_id']} from cache -->" );
786 }
787 } else {
788 // Render/generate the table HTML, as no cache is to be used.
789 $_render->set_input( $table, $render_options );
790 $output = $_render->get_output( 'html' );
791 }
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
844 // Maybe print a list of used render options.
845 if ( $render_options['shortcode_debug'] && is_user_logged_in() ) {
846 $output .= '<pre>' . var_export( $render_options, true ) . '</pre>'; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
847 }
848
849 return $output;
850 }
851
852 /**
853 * Handle Shortcode [table-info id=<ID> field=<name> /].
854 *
855 * @since 1.0.0
856 *
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 /].
858 * @return string Text that replaces the Shortcode (error message or asked-for information).
859 */
860 public function shortcode_table_info( /* array|string */ $shortcode_atts ): string {
861 $shortcode_atts = (array) $shortcode_atts;
862
863 // Parse Shortcode attributes, only allow those that are specified.
864 $default_shortcode_atts = array(
865 'id' => '',
866 'field' => '',
867 'format' => '',
868 );
869 /**
870 * Filters the available/default attributes for the [table-info] Shortcode.
871 *
872 * @since 1.0.0
873 *
874 * @param array<string, mixed> $default_shortcode_atts The [table-info] Shortcode default attributes.
875 */
876 $default_shortcode_atts = apply_filters( 'tablepress_shortcode_table_info_default_shortcode_atts', $default_shortcode_atts );
877 $shortcode_atts = shortcode_atts( $default_shortcode_atts, $shortcode_atts ); // Optional third argument left out on purpose. Use filter in the next line instead.
878 /**
879 * Filters the attributes that were passed to the [table-info] Shortcode.
880 *
881 * @since 1.0.0
882 *
883 * @param array<string, mixed> $shortcode_atts The attributes passed to the [table-info] Shortcode.
884 */
885 $shortcode_atts = apply_filters( 'tablepress_shortcode_table_info_shortcode_atts', $shortcode_atts );
886
887 /**
888 * Filters whether the output of the [table-info] Shortcode is overwritten/short-circuited.
889 *
890 * @since 1.0.0
891 *
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.
894 */
895 $overwrite = apply_filters( 'tablepress_shortcode_table_info_overwrite', false, $shortcode_atts );
896 if ( is_string( $overwrite ) ) {
897 return $overwrite;
898 }
899
900 // Check, if a table with the given ID exists.
901 $table_id = preg_replace( '/[^a-zA-Z0-9_-]/', '', $shortcode_atts['id'] );
902 if ( ! TablePress::$model_table->table_exists( $table_id ) ) {
903 $message = "&#91;table “{$table_id}” not found /&#93;<br />\n";
904 /** This filter is documented in controllers/controller-frontend.php */
905 $message = apply_filters( 'tablepress_table_not_found_message', $message, $table_id );
906 return $message;
907 }
908
909 // Load table, with table data, options, and visibility settings.
910 $table = TablePress::$model_table->load( $table_id, true, true );
911 if ( is_wp_error( $table ) ) {
912 $message = "&#91;table “{$table_id}” could not be loaded /&#93;<br />\n";
913 /** This filter is documented in controllers/controller-frontend.php */
914 $message = apply_filters( 'tablepress_table_load_error_message', $message, $table_id, $table );
915 return $message;
916 }
917
918 $field = (string) preg_replace( '/[^a-z_]/', '', strtolower( $shortcode_atts['field'] ) );
919 $format = (string) preg_replace( '/[^a-z]/', '', strtolower( $shortcode_atts['format'] ) );
920
921 // Generate output, depending on what information (field) was asked for.
922 switch ( $field ) {
923 case 'name':
924 case 'description':
925 $output = $table[ $field ];
926 break;
927 case 'last_modified':
928 switch ( $format ) {
929 case 'raw':
930 case 'mysql':
931 $output = $table['last_modified'];
932 break;
933 case 'human':
934 $modified_timestamp = date_create( $table['last_modified'], wp_timezone() );
935 if ( false === $modified_timestamp ) {
936 $modified_timestamp = $table['last_modified'];
937 } else {
938 $modified_timestamp = $modified_timestamp->getTimestamp();
939 }
940 $current_timestamp = time();
941 $time_diff = $current_timestamp - $modified_timestamp;
942 // Time difference is only shown up to one week.
943 if ( $time_diff >= 0 && $time_diff < WEEK_IN_SECONDS ) {
944 $output = sprintf( __( '%s ago', 'default' ), human_time_diff( $modified_timestamp, $current_timestamp ) );
945 } else {
946 $output = TablePress::format_datetime( $table['last_modified'], '<br />' );
947 }
948 break;
949 case 'date':
950 $output = TablePress::format_datetime( $table['last_modified'], get_option( 'date_format' ) );
951 break;
952 case 'time':
953 $output = TablePress::format_datetime( $table['last_modified'], get_option( 'time_format' ) );
954 break;
955 default:
956 $output = TablePress::format_datetime( $table['last_modified'] );
957 break;
958 }
959 break;
960 case 'last_editor':
961 $output = TablePress::get_user_display_name( $table['options']['last_editor'] );
962 break;
963 case 'author':
964 $output = TablePress::get_user_display_name( $table['author'] );
965 break;
966 case 'number_rows':
967 $output = count( $table['data'] );
968 if ( 'raw' !== $format ) {
969 $output -= $table['options']['table_head'];
970 $output -= $table['options']['table_foot'];
971 }
972 break;
973 case 'number_columns':
974 $output = count( $table['data'][0] );
975 break;
976 default:
977 $output = "&#91;table-info field “{$field}” not found in table “{$table_id}” /&#93;<br />\n";
978 /**
979 * Filters the "table info field not found" message.
980 *
981 * @since 1.0.0
982 *
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.
987 */
988 $output = apply_filters( 'tablepress_table_info_not_found_message', $output, $table, $field, $format );
989 }
990
991 /**
992 * Filters the output of the [table-info] Shortcode.
993 *
994 * @since 1.0.0
995 *
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.
999 */
1000 $output = apply_filters( 'tablepress_shortcode_table_info_output', $output, $table, $shortcode_atts );
1001 return $output;
1002 }
1003
1004 /**
1005 * Expand WP Search to also find posts and pages that have a search term in a table that is shown in them.
1006 *
1007 * This is done by looping through all search terms and TablePress tables and searching there for the search term,
1008 * 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
1009 * Shortcode for one of these tables in their content.
1010 *
1011 * @since 1.0.0
1012 *
1013 * @global wpdb $wpdb WordPress database abstraction object.
1014 *
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.
1016 * @return string Eventually extended SQL "WHERE" clause, to also find posts/pages with Shortcodes in them.
1017 */
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
1021 global $wpdb;
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
1028 if ( ! is_search() || ! is_main_query() ) {
1029 return $search_sql;
1030 }
1031
1032 // Get variable that contains all search terms, parsed from $_GET['s'] by WP.
1033 $search_terms = get_query_var( 'search_terms' );
1034 if ( empty( $search_terms ) || ! is_array( $search_terms ) ) {
1035 return $search_sql;
1036 }
1037
1038 // Load all table IDs and prime post meta cache for cached access to options and visibility settings of the tables, don't run filter hook.
1039 $table_ids = TablePress::$model_table->load_all( true, false );
1040 // Array of all search words that were found, and the table IDs where they were found.
1041 $query_result = array();
1042
1043 foreach ( $table_ids as $table_id ) {
1044 // Load table, with table data, options, and visibility settings.
1045 $table = TablePress::$model_table->load( $table_id, true, true );
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.
1053 if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
1054 continue;
1055 }
1056
1057 foreach ( $search_terms as $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 ) ) ) {
1060 // Found the search term in the name or description (and they are shown).
1061 $query_result[ $search_term ][] = $table_id; // Add table ID to result list.
1062 // No need to continue searching this search term in this table.
1063 continue;
1064 }
1065
1066 // Search search term in visible table cells (without taking Shortcode parameters into account!).
1067 foreach ( $table['data'] as $row_idx => $table_row ) {
1068 if ( 0 === $table['visibility']['rows'][ $row_idx ] ) {
1069 // Row is hidden, so don't search in it.
1070 continue;
1071 }
1072 foreach ( $table_row as $col_idx => $table_cell ) {
1073 if ( 0 === $table['visibility']['columns'][ $col_idx ] ) {
1074 // Column is hidden, so don't search in it.
1075 continue;
1076 }
1077 // @todo Cells are not evaluated here, so math formulas are searched.
1078 if ( false !== stripos( $table_cell, (string) $search_term ) ) {
1079 // Found the search term in the cell content.
1080 $query_result[ $search_term ][] = $table_id; // Add table ID to result list
1081 // No need to continue searching this search term in this table.
1082 continue 3;
1083 }
1084 }
1085 }
1086 }
1087 }
1088
1089 // For all found table IDs for each search term, add additional OR statement to the SQL "WHERE" clause.
1090
1091 // If $_GET['exact'] is set, WordPress doesn't use % in SQL LIKE clauses.
1092 $exact = get_query_var( 'exact' );
1093 $n = ( empty( $exact ) ) ? '%' : '';
1094 $search_sql = $wpdb->remove_placeholder_escape( $search_sql );
1095 foreach ( $query_result as $search_term => $table_ids ) {
1096 $search_term = esc_sql( $wpdb->esc_like( $search_term ) );
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.)
1098 $table_ids = implode( '|', $table_ids );
1099 $regexp = '\\\\[' . TablePress::$shortcode . ' id=(["\\\']?)(' . $table_ids . ')([\]"\\\' /])'; // ' needs to be single escaped, [ double escaped (with \\) in mySQL
1100 $new_or = $old_or . " OR ({$wpdb->posts}.post_content REGEXP '{$regexp}')";
1101 $search_sql = str_replace( $old_or, $new_or, $search_sql );
1102 }
1103 $search_sql = $wpdb->add_placeholder_escape( $search_sql );
1104
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 );
1130 }
1131
1132 } // class TablePress_Frontend_Controller
1133