PluginProbe
TablePress – Tables in WordPress made easy / 2.4.4
TablePress – Tables in WordPress made easy v2.4.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
tablepress / controllers / controller-frontend.php

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

1,025 lines 41.0 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 * List of tables that are shown for the current request.
26 *
27 * @since 1.0.0
28 * @var array<string, array{count: int, instances: array<string, array<string, mixed>>}>
29 */
30 protected $shown_tables = array();
31
32 /**
33 * Initiate Frontend functionality.
34 *
35 * @since 1.0.0
36 */
37 public function __construct() {
38 parent::__construct();
39
40 /**
41 * Filters whether the TablePress Default CSS code shall be loaded.
42 *
43 * @since 1.0.0
44 *
45 * @param bool $use Whether the Default CSS shall be loaded. Default true.
46 */
47 if ( apply_filters( 'tablepress_use_default_css', true ) || TablePress::$model_options->get( 'use_custom_css' ) ) {
48 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_css' ) );
49 }
50
51 // Add DataTables invocation calls.
52 add_action( 'wp_print_footer_scripts', array( $this, 'add_datatables_calls' ), 11 ); // After inclusion of files.
53
54 // Register TablePress Shortcodes. Priority 20 is kept for backwards-compatibility purposes.
55 add_action( 'init', array( $this, 'init_shortcodes' ), 20 );
56
57 /**
58 * Filters whether the WordPress search shall also search TablePress tables.
59 *
60 * @since 1.0.0
61 *
62 * @param bool $search Whether the TablePress tables shall be searched. Default true.
63 */
64 if ( apply_filters( 'tablepress_wp_search_integration', true ) ) {
65 // 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.
66 add_filter( 'posts_search', array( $this, 'posts_search_filter' ) );
67 }
68
69 /**
70 * Load TablePress Template Tag functions.
71 */
72 TablePress::load_file( 'template-tag-functions.php', 'controllers' );
73
74 /**
75 * Register the tablepress/table block and its dependencies.
76 */
77 register_block_type(
78 TABLEPRESS_ABSPATH . 'blocks/table/',
79 array(
80 'render_callback' => array( $this, 'table_block_render_callback' ),
81 )
82 );
83 }
84
85 /**
86 * Register TablePress Shortcodes.
87 *
88 * @since 1.0.0
89 */
90 public function init_shortcodes(): void {
91 add_shortcode( TablePress::$shortcode, array( $this, 'shortcode_table' ) );
92 add_shortcode( TablePress::$shortcode_info, array( $this, 'shortcode_table_info' ) );
93 }
94
95 /**
96 * Enqueue CSS files for default CSS and "Custom CSS" (if desired).
97 *
98 * @since 1.0.0
99 */
100 public function enqueue_css(): void {
101 /** This filter is documented in controllers/controller-frontend.php */
102 $use_default_css = apply_filters( 'tablepress_use_default_css', true );
103 $custom_css = TablePress::$model_options->get( 'custom_css' );
104 $use_custom_css = ( TablePress::$model_options->get( 'use_custom_css' ) && '' !== $custom_css );
105 $use_custom_css_file = ( $use_custom_css && TablePress::$model_options->get( 'use_custom_css_file' ) );
106 /**
107 * Filters the "Custom CSS" version number that is appended to the enqueued CSS files
108 *
109 * @since 1.0.0
110 *
111 * @param int $version The "Custom CSS" version.
112 */
113 $custom_css_version = (string) apply_filters( 'tablepress_custom_css_version', TablePress::$model_options->get( 'custom_css_version' ) );
114
115 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
116
117 // Determine Default CSS URL.
118 $rtl = ( is_rtl() ) ? '-rtl' : '';
119 $unfiltered_default_css_url = plugins_url( "css/build/default{$rtl}.css", TABLEPRESS__FILE__ );
120 /**
121 * Filters the URL from which the TablePress Default CSS file is loaded.
122 *
123 * @since 1.0.0
124 *
125 * @param string $unfiltered_default_css_url URL of the TablePress Default CSS file.
126 */
127 $default_css_url = apply_filters( 'tablepress_default_css_url', $unfiltered_default_css_url );
128
129 $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' ) );
130
131 if ( $use_custom_css_combined_file ) {
132 $custom_css_combined_url = $tablepress_css->get_custom_css_location( 'combined', 'url' );
133 // Need to use 'tablepress-default' instead of 'tablepress-combined' to not break existing TablePress Extensions.
134 wp_enqueue_style( 'tablepress-default', $custom_css_combined_url, array(), $custom_css_version );
135 } else {
136 $custom_css_dependencies = array();
137 if ( $use_default_css ) {
138 wp_enqueue_style( 'tablepress-default', $default_css_url, array(), TablePress::version );
139 // Add dependency to make sure that Custom CSS is printed after Default CSS.
140 $custom_css_dependencies[] = 'tablepress-default';
141 }
142
143 $use_custom_css_minified_file = ( $use_custom_css_file && ! SCRIPT_DEBUG && $tablepress_css->load_custom_css_from_file( 'minified' ) );
144 if ( $use_custom_css_minified_file ) {
145 $custom_css_minified_url = $tablepress_css->get_custom_css_location( 'minified', 'url' );
146 wp_enqueue_style( 'tablepress-custom', $custom_css_minified_url, $custom_css_dependencies, $custom_css_version );
147 return;
148 }
149
150 $use_custom_css_normal_file = ( $use_custom_css_file && $tablepress_css->load_custom_css_from_file( 'normal' ) );
151 if ( $use_custom_css_normal_file ) {
152 $custom_css_normal_url = $tablepress_css->get_custom_css_location( 'normal', 'url' );
153 wp_enqueue_style( 'tablepress-custom', $custom_css_normal_url, $custom_css_dependencies, $custom_css_version );
154 return;
155 }
156
157 if ( $use_custom_css ) {
158 // Get "Custom CSS" from options, try minified Custom CSS first.
159 $custom_css_minified = TablePress::$model_options->get( 'custom_css_minified' );
160 if ( ! empty( $custom_css_minified ) ) {
161 $custom_css = $custom_css_minified;
162 }
163 /**
164 * Filters the "Custom CSS" code that is to be loaded as inline CSS.
165 *
166 * @since 1.0.0
167 *
168 * @param string $custom_css The "Custom CSS" code.
169 */
170 $custom_css = apply_filters( 'tablepress_custom_css', $custom_css );
171 if ( ! empty( $custom_css ) ) {
172 // wp_add_inline_style() requires a loaded CSS file, so we have to work around that if "Default CSS" is disabled.
173 if ( $use_default_css ) {
174 // Handle of the file to which the <style> shall be appended.
175 wp_add_inline_style( 'tablepress-default', $custom_css );
176 } else {
177 add_action( 'wp_head', array( $this, '_print_custom_css' ), 8 ); // Priority 8 to hook in right after WP_Styles has been processed.
178 }
179 }
180 }
181 }
182 }
183
184 /**
185 * Print "Custom CSS" to "wp_head" inline.
186 *
187 * This is necessary if "Default CSS" is off, and saving "Custom CSS" to a file is not possible.
188 *
189 * @since 1.0.0
190 */
191 public function _print_custom_css(): void {
192 // Get "Custom CSS" from options, try minified Custom CSS first.
193 $custom_css = TablePress::$model_options->get( 'custom_css_minified' );
194 if ( empty( $custom_css ) ) {
195 $custom_css = TablePress::$model_options->get( 'custom_css' );
196 }
197 /** This filter is documented in controllers/controller-frontend.php */
198 $custom_css = apply_filters( 'tablepress_custom_css', $custom_css );
199 echo "<style>\n{$custom_css}\n</style>\n";
200 }
201
202 /**
203 * Enqueue the DataTables JavaScript library and its dependencies.
204 *
205 * @since 1.0.0
206 */
207 protected function _enqueue_datatables(): void {
208 $js_file = 'js/jquery.datatables.min.js';
209 $js_url = plugins_url( $js_file, TABLEPRESS__FILE__ );
210 /**
211 * Filters the URL from which the DataTables JavaScript library file is loaded.
212 *
213 * @since 1.0.0
214 *
215 * @param string $js_url URL of the DataTables JS library file.
216 * @param string $js_file Path and file name of the DataTables JS library file.
217 */
218 $js_url = apply_filters( 'tablepress_datatables_js_url', $js_url, $js_file );
219 wp_enqueue_script( 'tablepress-datatables', $js_url, array( 'jquery-core' ), TablePress::version, true );
220 }
221
222 /**
223 * Add JS code for invocation of DataTables JS library.
224 *
225 * @since 1.0.0
226 */
227 public function add_datatables_calls(): void {
228 // Prevent repeated execution (which would lead to DataTables error messages) via a static variable.
229 static $datatables_calls_printed = false;
230 if ( $datatables_calls_printed ) {
231 return;
232 }
233
234 if ( empty( $this->shown_tables ) ) {
235 // There are no tables with activated DataTables on the page that is currently rendered.
236 return;
237 }
238
239 /*
240 * Don't add the DataTables function calls in the scope of the block editor.
241 * Otherwise, this causes a script error in the block editor iframe.
242 */
243 if ( function_exists( 'get_current_screen' ) ) {
244 $current_screen = get_current_screen();
245 if ( ( $current_screen instanceof WP_Screen ) && $current_screen->is_block_editor() ) {
246 return;
247 }
248 }
249
250 // Storage for the DataTables language strings.
251 $datatables_language = array();
252 // Generate the specific JS commands, depending on chosen features on the "Edit" screen and the Shortcode parameters.
253 $commands = array();
254
255 foreach ( $this->shown_tables as $table_id => $table_store ) {
256 $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.
257
258 if ( empty( $table_store['instances'] ) ) {
259 continue;
260 }
261
262 foreach ( $table_store['instances'] as $html_id => $js_options ) {
263 $parameters = array();
264
265 // Settle dependencies/conflicts between certain features.
266 if ( false !== $js_options['datatables_scrolly'] ) { // datatables_scrolly can be a string, so that the explicit `false` check is needed.
267 // Vertical scrolling and pagination don't work together.
268 $js_options['datatables_paginate'] = false;
269 }
270 // Sanitize, as it may come from a Shortcode attribute.
271 $js_options['datatables_paginate_entries'] = (int) $js_options['datatables_paginate_entries'];
272
273 /*
274 * DataTables language/translation handling.
275 */
276
277 /**
278 * Filters the locale/language for the DataTables JavaScript library.
279 *
280 * @since 1.0.0
281 *
282 * @param string $locale The DataTables JS library locale.
283 * @param string $table_id The current table ID.
284 */
285 $datatables_locale = apply_filters( 'tablepress_datatables_locale', $js_options['datatables_locale'], $table_id );
286
287 // Only load each locale's language file once.
288 if ( ! isset( $datatables_language[ $datatables_locale ] ) ) {
289 $orig_language_file = TABLEPRESS_ABSPATH . "i18n/datatables/lang-{$datatables_locale}.php";
290
291 /**
292 * Filters the language file path for the DataTables JavaScript library.
293 *
294 * PHP files that return an array and JSON files are supported.
295 * The JSON file method is deprecated and should no longer be used.
296 *
297 * @since 1.0.0
298 *
299 * @param string $orig_language_file Language file path for the DataTables JS library.
300 * @param string $datatables_locale Current locale/language for the DataTables JS library.
301 * @param string $tablepress_abspath Base path of the TablePress plugin.
302 */
303 $language_file = apply_filters( 'tablepress_datatables_language_file', $orig_language_file, $datatables_locale, TABLEPRESS_ABSPATH );
304
305 /*
306 * Load translation file if it's not "en_US" (included as the default in DataTables)
307 * or if the filter was used to change the language file, and the language file exists.
308 * Otherwise, use an empty en_US placeholder, so that the strings are filterable later.
309 */
310 if ( ( 'en_US' !== $datatables_locale || $orig_language_file !== $language_file ) && file_exists( $language_file ) ) {
311 if ( str_ends_with( $language_file, '.php' ) ) {
312 $datatables_strings = require $language_file;
313 if ( ! is_array( $datatables_strings ) ) {
314 $datatables_strings = array();
315 }
316 } elseif ( str_ends_with( $language_file, '.json' ) ) {
317 $datatables_strings = file_get_contents( $language_file );
318 $datatables_strings = json_decode( $datatables_strings, true ); // @phpstan-ignore-line
319 // Check if JSON could be decoded.
320 if ( is_null( $datatables_strings ) ) {
321 $datatables_strings = array();
322 }
323 $datatables_strings = (array) $datatables_strings;
324 } else {
325 // The filtered language file exists, but is not a .php or .json file, so don't use it.
326 $datatables_strings = array();
327 }
328 } else {
329 // If no translation file for the defined locale exists or is needed, use "en_US", as that's built-in.
330 $datatables_locale = 'en_US';
331 $datatables_strings = array();
332 }
333
334 /**
335 * Filters the language strings for the DataTables JavaScript library's features.
336 *
337 * @since 2.0.0
338 *
339 * @param array<string, mixed> $datatables_strings The language strings for DataTables.
340 * @param string $datatables_locale Current locale/language for the DataTables JS library.
341 */
342 $datatables_language[ $datatables_locale ] = apply_filters( 'tablepress_datatables_language_strings', $datatables_strings, $datatables_locale );
343 }
344 $parameters['language'] = '"language":DT_language["' . $datatables_locale . '"]';
345
346 // These parameters need to be added for performance gain or to overwrite unwanted default behavior.
347 if ( $js_options['datatables_sort'] ) {
348 // No initial sort.
349 $parameters['order'] = '"order":[]';
350 // Don't add additional classes, to speed up sorting.
351 $parameters['orderClasses'] = '"orderClasses":false';
352 }
353
354 // Alternating row colors is default, so remove them if not wanted with [].
355 $parameters['stripeClasses'] = '"stripeClasses":' . ( ( $js_options['alternating_row_colors'] ) ? '["even","odd"]' : '[]' );
356
357 // 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.
358 if ( ! $js_options['datatables_sort'] ) {
359 $parameters['ordering'] = '"ordering":false';
360 }
361 if ( $js_options['datatables_paginate'] ) {
362 $parameters['pagingType'] = '"pagingType":"simple"';
363 if ( $js_options['datatables_lengthchange'] ) {
364 $length_menu = array( 10, 25, 50, 100 );
365 if ( ! in_array( $js_options['datatables_paginate_entries'], $length_menu, true ) ) {
366 $length_menu[] = $js_options['datatables_paginate_entries'];
367 sort( $length_menu, SORT_NUMERIC );
368 $parameters['lengthMenu'] = '"lengthMenu":[' . implode( ',', $length_menu ) . ']';
369 }
370 } else {
371 $parameters['lengthChange'] = '"lengthChange":false';
372 }
373 if ( 10 !== $js_options['datatables_paginate_entries'] ) {
374 $parameters['pageLength'] = '"pageLength":' . $js_options['datatables_paginate_entries'];
375 }
376 } else {
377 $parameters['paging'] = '"paging":false';
378 }
379 if ( ! $js_options['datatables_filter'] ) {
380 $parameters['searching'] = '"searching":false';
381 }
382 if ( ! $js_options['datatables_info'] ) {
383 $parameters['info'] = '"info":false';
384 }
385 if ( $js_options['datatables_scrollx'] ) {
386 $parameters['scrollX'] = '"scrollX":true';
387 }
388 if ( false !== $js_options['datatables_scrolly'] ) {
389 $parameters['scrollY'] = '"scrollY":"' . preg_replace( '#[^0-9a-z.%]#', '', $js_options['datatables_scrolly'] ) . '"';
390 $parameters['scrollCollapse'] = '"scrollCollapse":true';
391 }
392 if ( ! empty( $js_options['datatables_custom_commands'] ) ) {
393 $parameters['custom_commands'] = $js_options['datatables_custom_commands'];
394 }
395
396 /**
397 * Filters the parameters that are passed to the DataTables JavaScript library.
398 *
399 * @since 1.0.0
400 *
401 * @param array<string, mixed> $parameters The parameters for the DataTables JS library.
402 * @param string $table_id The current table ID.
403 * @param string $html_id The ID of the table HTML element.
404 * @param array<string, mixed> $js_options The options for the JS library.
405 */
406 $parameters = apply_filters( 'tablepress_datatables_parameters', $parameters, $table_id, $html_id, $js_options );
407
408 // If an existing parameter (in the from `"parameter":`) is set in the "Custom Commands", remove its default value.
409 if ( isset( $parameters['custom_commands'] ) ) {
410 foreach ( array_keys( $parameters ) as $maybe_overwritten_parameter ) {
411 if ( str_contains( $parameters['custom_commands'], "\"{$maybe_overwritten_parameter}\":" ) ) {
412 unset( $parameters[ $maybe_overwritten_parameter ] );
413 }
414 }
415 }
416
417 $parameters = implode( ',', $parameters );
418 $parameters = ( ! empty( $parameters ) ) ? '{' . $parameters . '}' : '';
419
420 $command = "$('#{$html_id}').DataTable({$parameters});";
421 /**
422 * Filters the JavaScript command that invokes the DataTables JavaScript library on one table.
423 *
424 * @since 1.0.0
425 *
426 * @param string $command The JS command for the DataTables JS library.
427 * @param string $html_id The ID of the table HTML element.
428 * @param string $parameters The parameters for the DataTables JS library.
429 * @param string $table_id The current table ID.
430 * @param array<string, mixed> $js_options The options for the JS library.
431 */
432 $command = apply_filters( 'tablepress_datatables_command', $command, $html_id, $parameters, $table_id, $js_options );
433 if ( ! empty( $command ) ) {
434 $commands[] = $command;
435 }
436 } // foreach table instance
437 } // foreach table ID
438
439 // DataTables language/translation handling.
440 if ( ! empty( $datatables_language ) ) {
441 $datatables_language = wp_json_encode( $datatables_language, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT );
442 $datatables_language = "var DT_language={$datatables_language};\n";
443 } else {
444 $datatables_language = '';
445 }
446
447 $commands = implode( "\n", $commands );
448 /**
449 * Filters the JavaScript commands that invoke the DataTables JavaScript library on all tables on the page.
450 *
451 * @since 1.0.0
452 *
453 * @param string $commands The JS commands for the DataTables JS library.
454 */
455 $commands = apply_filters( 'tablepress_all_datatables_commands', $commands );
456 if ( '' === $commands ) {
457 return;
458 }
459
460 $script_type_attr = current_theme_supports( 'html5', 'script' ) ? '' : ' type="text/javascript"';
461
462 $js_wrapper = <<<'JS'
463 <script%3$s>
464 jQuery(function($){
465 %1$s%2$s
466 });
467 </script>
468 JS;
469 /**
470 * Filters the script/jQuery wrapper code for the DataTables commands calls.
471 *
472 * @since 1.14.0
473 *
474 * @param string $js_wrapper Default script/jQuery wrapper code for the DataTables commands calls.
475 */
476 $js_wrapper = apply_filters( 'tablepress_all_datatables_commands_wrapper', $js_wrapper );
477 printf( $js_wrapper, $datatables_language, $commands, $script_type_attr );
478
479 // Prevent repeated execution (which would lead to DataTables error messages) via a static variable.
480 $datatables_calls_printed = true;
481 }
482
483 /**
484 * Handle Shortcode [table id=<ID> /].
485 *
486 * @since 1.0.0
487 *
488 * @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 /].
489 * @return string Resulting HTML code for the table with the ID <ID>.
490 */
491 public function shortcode_table( /* array|string */ $shortcode_atts ): string {
492 $shortcode_atts = (array) $shortcode_atts;
493
494 $_render = TablePress::load_class( 'TablePress_Render', 'class-render.php', 'classes' );
495
496 $default_shortcode_atts = $_render->get_default_render_options();
497 /**
498 * Filters the available/default attributes for the [table] Shortcode.
499 *
500 * @since 1.0.0
501 *
502 * @param array<string, mixed> $default_shortcode_atts The [table] Shortcode default attributes.
503 */
504 $default_shortcode_atts = apply_filters( 'tablepress_shortcode_table_default_shortcode_atts', $default_shortcode_atts );
505 // Parse Shortcode attributes, only allow those that are specified.
506 $shortcode_atts = shortcode_atts( $default_shortcode_atts, $shortcode_atts ); // Optional third argument left out on purpose. Use filter in the next line instead.
507 /**
508 * Filters the attributes that were passed to the [table] Shortcode.
509 *
510 * @since 1.0.0
511 *
512 * @param array<string, mixed> $shortcode_atts The attributes passed to the [table] Shortcode.
513 */
514 $shortcode_atts = apply_filters( 'tablepress_shortcode_table_shortcode_atts', $shortcode_atts );
515
516 // Check, if a table with the given ID exists.
517 $table_id = (string) preg_replace( '/[^a-zA-Z0-9_-]/', '', $shortcode_atts['id'] );
518 if ( ! TablePress::$model_table->table_exists( $table_id ) ) {
519 $message = "&#91;table “{$table_id}” not found /&#93;<br />\n";
520 /**
521 * Filters the "Table not found" message.
522 *
523 * @since 1.0.0
524 *
525 * @param string $message The "Table not found" message.
526 * @param string $table_id The current table ID.
527 */
528 $message = apply_filters( 'tablepress_table_not_found_message', $message, $table_id );
529 return $message;
530 }
531
532 // Load table, with table data, options, and visibility settings.
533 $table = TablePress::$model_table->load( $table_id, true, true );
534 if ( is_wp_error( $table ) ) {
535 $message = "&#91;table “{$table_id}” could not be loaded /&#93;<br />\n";
536 /**
537 * Filters the "Table could not be loaded" message.
538 *
539 * @since 1.0.0
540 *
541 * @param string $message The "Table could not be loaded" message.
542 * @param string $table_id The current table ID.
543 * @param WP_Error $table The error object for the table.
544 */
545 $message = apply_filters( 'tablepress_table_load_error_message', $message, $table_id, $table );
546 return $message;
547 }
548 if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
549 $message = "<div>Attention: The internal data of table “{$table_id}” is corrupted!</div>";
550 /**
551 * Filters the "Table data is corrupted" message.
552 *
553 * @since 1.0.0
554 *
555 * @param string $message The "Table data is corrupted" message.
556 * @param string $table_id The current table ID.
557 * @param string $json_error The JSON error with information about the corrupted table.
558 */
559 $message = apply_filters( 'tablepress_table_corrupted_message', $message, $table_id, $table['json_error'] );
560 return $message;
561 }
562
563 /**
564 * Filters whether the "datatables_custom_commands" Shortcode parameter is disabled.
565 *
566 * By default, the "datatables_custom_commands" Shortcode parameter is disabled for security reasons.
567 *
568 * @since 1.0.0
569 *
570 * @param bool $disable Whether to disable the "datatables_custom_commands" Shortcode parameter. Default true.
571 */
572 if ( ! is_null( $shortcode_atts['datatables_custom_commands'] ) && apply_filters( 'tablepress_disable_custom_commands_shortcode_parameter', true ) ) {
573 $shortcode_atts['datatables_custom_commands'] = null;
574 }
575
576 // Determine options to use (if set in Shortcode, use those, otherwise use stored options, from the "Edit" screen).
577 $render_options = array();
578 foreach ( $shortcode_atts as $key => $value ) {
579 if ( is_null( $value ) && isset( $table['options'][ $key ] ) ) {
580 // Use the table's stored option value, if the Shortcode parameter was not set.
581 $render_options[ $key ] = $table['options'][ $key ];
582 } elseif ( is_string( $value ) ) {
583 // Convert strings 'true' or 'false' to boolean, keep others.
584 $value_lowercase = strtolower( $value );
585 if ( 'true' === $value_lowercase ) {
586 $render_options[ $key ] = true;
587 } elseif ( 'false' === $value_lowercase ) {
588 $render_options[ $key ] = false;
589 } else {
590 $render_options[ $key ] = $value;
591 }
592 } else {
593 // Keep all other values.
594 $render_options[ $key ] = $value;
595 }
596 }
597
598 // Generate unique HTML ID, depending on how often this table has already been shown on this page.
599 if ( ! isset( $this->shown_tables[ $table_id ] ) ) {
600 $this->shown_tables[ $table_id ] = array(
601 'count' => 0,
602 'instances' => array(),
603 );
604 }
605 ++$this->shown_tables[ $table_id ]['count'];
606 $count = $this->shown_tables[ $table_id ]['count'];
607 $render_options['html_id'] = "tablepress-{$table_id}";
608 if ( $count > 1 ) {
609 $render_options['html_id'] .= "-no-{$count}";
610 }
611 /**
612 * Filters the ID of the table HTML element.
613 *
614 * @since 1.0.0
615 *
616 * @param string $html_id The ID of the table HTML element.
617 * @param string $table_id The current table ID.
618 * @param int $count Number of copies of the table with this table ID on the page.
619 */
620 $render_options['html_id'] = apply_filters( 'tablepress_html_id', $render_options['html_id'], $table_id, $count );
621
622 // Generate the "Edit Table" link.
623 $render_options['edit_table_url'] = '';
624 /**
625 * Filters whether the "Edit" link below the table shall be shown.
626 *
627 * The "Edit" link is only shown to logged-in users who possess the necessary capability to edit the table.
628 *
629 * @since 1.0.0
630 *
631 * @param bool $show Whether to show the "Edit" link below the table. Default true.
632 * @param string $table_id The current table ID.
633 */
634 if ( is_user_logged_in() && apply_filters( 'tablepress_edit_link_below_table', true, $table['id'] ) && current_user_can( 'tablepress_edit_table', $table['id'] ) ) {
635 $render_options['edit_table_url'] = TablePress::url( array( 'action' => 'edit', 'table_id' => $table['id'] ) );
636 }
637
638 /**
639 * Filters the render options for the table.
640 *
641 * The render options are determined from the settings on a table's "Edit" screen and the Shortcode parameters.
642 *
643 * @since 1.0.0
644 *
645 * @param array<string, mixed> $render_options The render options for the table.
646 * @param array<string, mixed> $table The current table.
647 */
648 $render_options = apply_filters( 'tablepress_table_render_options', $render_options, $table );
649
650 // Check if table output shall and can be loaded from the transient cache, otherwise generate the output.
651 if ( $render_options['cache_table_output'] && ! is_user_logged_in() ) {
652 // Hash the Render Options array to get a unique cache identifier.
653 $table_hash = md5( wp_json_encode( $render_options, TABLEPRESS_JSON_OPTIONS ) ); // @phpstan-ignore-line
654 $transient_name = 'tablepress_' . $table_hash; // Attention: This string must not be longer than 45 characters!
655 $output = get_transient( $transient_name );
656 if ( false === $output || '' === $output ) {
657 // Render/generate the table HTML, as it was not found in the cache.
658 $_render->set_input( $table, $render_options );
659 $output = $_render->get_output( 'html' );
660 // Save render output in a transient, set cache timeout to 24 hours.
661 set_transient( $transient_name, $output, DAY_IN_SECONDS );
662 // Update output caches list transient (necessary for cache invalidation upon table saving).
663 $caches_list_transient_name = 'tablepress_c_' . md5( $table_id );
664 $caches_list = get_transient( $caches_list_transient_name );
665 if ( false === $caches_list ) {
666 $caches_list = array();
667 } else {
668 $caches_list = (array) json_decode( $caches_list, true );
669 }
670 if ( ! in_array( $transient_name, $caches_list, true ) ) {
671 $caches_list[] = $transient_name;
672 }
673 set_transient( $caches_list_transient_name, wp_json_encode( $caches_list, TABLEPRESS_JSON_OPTIONS ), 2 * DAY_IN_SECONDS );
674 } else {
675 /**
676 * Filters the cache hit comment message.
677 *
678 * @since 1.0.0
679 *
680 * @param string $comment The cache hit comment message.
681 */
682 $output .= apply_filters( 'tablepress_cache_hit_comment', "<!-- #{$render_options['html_id']} from cache -->" );
683 }
684 } else {
685 // Render/generate the table HTML, as no cache is to be used.
686 $_render->set_input( $table, $render_options );
687 $output = $_render->get_output( 'html' );
688 }
689
690 // 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.
691 if ( $render_options['use_datatables']
692 && $render_options['table_head']
693 && str_contains( $output, '<thead' ) // A `<thead>` tag is required.
694 && ! str_contains( $output, ' colspan="' ) // `colspan` attributes are forbidden.
695 && ! str_contains( $output, ' rowspan="' ) // `rowspan` attributes are forbidden.
696 ) {
697 // Get options for the DataTables JavaScript library from the table's render options.
698 $js_options = array();
699 foreach ( array(
700 'alternating_row_colors',
701 'datatables_sort',
702 'datatables_paginate',
703 'datatables_paginate',
704 'datatables_paginate_entries',
705 'datatables_lengthchange',
706 'datatables_filter',
707 'datatables_info',
708 'datatables_scrollx',
709 'datatables_scrolly',
710 'datatables_locale',
711 'datatables_custom_commands',
712 ) as $option ) {
713 $js_options[ $option ] = $render_options[ $option ];
714 }
715 /**
716 * Filters the JavaScript options for the table.
717 *
718 * The JavaScript options are determined from the settings on a table's "Edit" screen and the Shortcode parameters.
719 * They are part of the render options and can be overwritten with Shortcode parameters.
720 *
721 * @since 1.0.0
722 *
723 * @param array<string, mixed> $js_options The JavaScript options for the table.
724 * @param string $table_id The current table ID.
725 * @param array<string, mixed> $render_options The render options for the table.
726 */
727 $js_options = apply_filters( 'tablepress_table_js_options', $js_options, $table_id, $render_options );
728 $this->shown_tables[ $table_id ]['instances'][ (string) $render_options['html_id'] ] = $js_options;
729 $this->_enqueue_datatables();
730 }
731
732 // Maybe print a list of used render options.
733 if ( $render_options['shortcode_debug'] && is_user_logged_in() ) {
734 $output .= '<pre>' . var_export( $render_options, true ) . '</pre>'; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
735 }
736
737 return $output;
738 }
739
740 /**
741 * Handle Shortcode [table-info id=<ID> field=<name> /].
742 *
743 * @since 1.0.0
744 *
745 * @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 /].
746 * @return string Text that replaces the Shortcode (error message or asked-for information).
747 */
748 public function shortcode_table_info( /* array|string */ $shortcode_atts ): string {
749 $shortcode_atts = (array) $shortcode_atts;
750
751 // Parse Shortcode attributes, only allow those that are specified.
752 $default_shortcode_atts = array(
753 'id' => '',
754 'field' => '',
755 'format' => '',
756 );
757 /**
758 * Filters the available/default attributes for the [table-info] Shortcode.
759 *
760 * @since 1.0.0
761 *
762 * @param array<string, mixed> $default_shortcode_atts The [table-info] Shortcode default attributes.
763 */
764 $default_shortcode_atts = apply_filters( 'tablepress_shortcode_table_info_default_shortcode_atts', $default_shortcode_atts );
765 $shortcode_atts = shortcode_atts( $default_shortcode_atts, $shortcode_atts ); // Optional third argument left out on purpose. Use filter in the next line instead.
766 /**
767 * Filters the attributes that were passed to the [table-info] Shortcode.
768 *
769 * @since 1.0.0
770 *
771 * @param array<string, mixed> $shortcode_atts The attributes passed to the [table-info] Shortcode.
772 */
773 $shortcode_atts = apply_filters( 'tablepress_shortcode_table_info_shortcode_atts', $shortcode_atts );
774
775 /**
776 * Filters whether the output of the [table-info] Shortcode is overwritten/short-circuited.
777 *
778 * @since 1.0.0
779 *
780 * @param false|string $overwrite Whether the [table-info] output is overwritten. Return false for the regular content, and a string to overwrite the output.
781 * @param array<string, mixed> $shortcode_atts The attributes passed to the [table-info] Shortcode.
782 */
783 $overwrite = apply_filters( 'tablepress_shortcode_table_info_overwrite', false, $shortcode_atts );
784 if ( is_string( $overwrite ) ) {
785 return $overwrite;
786 }
787
788 // Check, if a table with the given ID exists.
789 $table_id = preg_replace( '/[^a-zA-Z0-9_-]/', '', $shortcode_atts['id'] );
790 if ( ! TablePress::$model_table->table_exists( $table_id ) ) {
791 $message = "&#91;table “{$table_id}” not found /&#93;<br />\n";
792 /** This filter is documented in controllers/controller-frontend.php */
793 $message = apply_filters( 'tablepress_table_not_found_message', $message, $table_id );
794 return $message;
795 }
796
797 // Load table, with table data, options, and visibility settings.
798 $table = TablePress::$model_table->load( $table_id, true, true );
799 if ( is_wp_error( $table ) ) {
800 $message = "&#91;table “{$table_id}” could not be loaded /&#93;<br />\n";
801 /** This filter is documented in controllers/controller-frontend.php */
802 $message = apply_filters( 'tablepress_table_load_error_message', $message, $table_id, $table );
803 return $message;
804 }
805
806 $field = (string) preg_replace( '/[^a-z_]/', '', strtolower( $shortcode_atts['field'] ) );
807 $format = (string) preg_replace( '/[^a-z]/', '', strtolower( $shortcode_atts['format'] ) );
808
809 // Generate output, depending on what information (field) was asked for.
810 switch ( $field ) {
811 case 'name':
812 case 'description':
813 $output = $table[ $field ];
814 break;
815 case 'last_modified':
816 switch ( $format ) {
817 case 'raw':
818 case 'mysql':
819 $output = $table['last_modified'];
820 break;
821 case 'human':
822 $modified_timestamp = date_create( $table['last_modified'], wp_timezone() );
823 if ( false === $modified_timestamp ) {
824 $modified_timestamp = $table['last_modified'];
825 } else {
826 $modified_timestamp = $modified_timestamp->getTimestamp();
827 }
828 $current_timestamp = time();
829 $time_diff = $current_timestamp - $modified_timestamp;
830 // Time difference is only shown up to one week.
831 if ( $time_diff >= 0 && $time_diff < WEEK_IN_SECONDS ) {
832 $output = sprintf( __( '%s ago', 'default' ), human_time_diff( $modified_timestamp, $current_timestamp ) );
833 } else {
834 $output = TablePress::format_datetime( $table['last_modified'], '<br />' );
835 }
836 break;
837 case 'date':
838 $output = TablePress::format_datetime( $table['last_modified'], get_option( 'date_format' ) );
839 break;
840 case 'time':
841 $output = TablePress::format_datetime( $table['last_modified'], get_option( 'time_format' ) );
842 break;
843 default:
844 $output = TablePress::format_datetime( $table['last_modified'] );
845 break;
846 }
847 break;
848 case 'last_editor':
849 $output = TablePress::get_user_display_name( $table['options']['last_editor'] );
850 break;
851 case 'author':
852 $output = TablePress::get_user_display_name( $table['author'] );
853 break;
854 case 'number_rows':
855 $output = count( $table['data'] );
856 if ( 'raw' !== $format ) {
857 if ( $table['options']['table_head'] ) {
858 --$output;
859 }
860 if ( $table['options']['table_foot'] ) {
861 --$output;
862 }
863 }
864 break;
865 case 'number_columns':
866 $output = count( $table['data'][0] );
867 break;
868 default:
869 $output = "&#91;table-info field “{$field}” not found in table “{$table_id}” /&#93;<br />\n";
870 /**
871 * Filters the "table info field not found" message.
872 *
873 * @since 1.0.0
874 *
875 * @param string $output The "table info field not found" message.
876 * @param array<string, mixed> $table The current table.
877 * @param string $field The field that was not found.
878 * @param string $format The return format for the field.
879 */
880 $output = apply_filters( 'tablepress_table_info_not_found_message', $output, $table, $field, $format );
881 }
882
883 /**
884 * Filters the output of the [table-info] Shortcode.
885 *
886 * @since 1.0.0
887 *
888 * @param string $output The output of the [table-info] Shortcode.
889 * @param array<string, mixed> $table The current table.
890 * @param array<string, mixed> $shortcode_atts The attributes passed to the [table-info] Shortcode.
891 */
892 $output = apply_filters( 'tablepress_shortcode_table_info_output', $output, $table, $shortcode_atts );
893 return $output;
894 }
895
896 /**
897 * Expand WP Search to also find posts and pages that have a search term in a table that is shown in them.
898 *
899 * This is done by looping through all search terms and TablePress tables and searching there for the search term,
900 * 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
901 * Shortcode for one of these tables in their content.
902 *
903 * @since 1.0.0
904 *
905 * @global wpdb $wpdb WordPress database abstraction object.
906 *
907 * @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.
908 * @return string Eventually extended SQL "WHERE" clause, to also find posts/pages with Shortcodes in them.
909 */
910 public function posts_search_filter( /* string */ $search_sql ): string {
911 // 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.
912
913 global $wpdb;
914
915 // Protect against cases where `null` is somehow passed to the filter hook callback.
916 if ( ! is_string( $search_sql ) ) {
917 return '';
918 }
919
920 if ( ! is_search() || ! is_main_query() ) {
921 return $search_sql;
922 }
923
924 // Get variable that contains all search terms, parsed from $_GET['s'] by WP.
925 $search_terms = get_query_var( 'search_terms' );
926 if ( empty( $search_terms ) || ! is_array( $search_terms ) ) {
927 return $search_sql;
928 }
929
930 // 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.
931 $table_ids = TablePress::$model_table->load_all( true, false );
932 // Array of all search words that were found, and the table IDs where they were found.
933 $query_result = array();
934
935 foreach ( $table_ids as $table_id ) {
936 // Load table, with table data, options, and visibility settings.
937 $table = TablePress::$model_table->load( $table_id, true, true );
938
939 // Skip tables that could not be loaded.
940 if ( is_wp_error( $table ) ) {
941 continue;
942 }
943
944 // Do not search in corrupted tables.
945 if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
946 continue;
947 }
948
949 foreach ( $search_terms as $search_term ) {
950 if ( ( $table['options']['print_name'] && false !== stripos( $table['name'], $search_term ) )
951 || ( $table['options']['print_description'] && false !== stripos( $table['description'], $search_term ) ) ) {
952 // Found the search term in the name or description (and they are shown).
953 $query_result[ $search_term ][] = $table_id; // Add table ID to result list.
954 // No need to continue searching this search term in this table.
955 continue;
956 }
957
958 // Search search term in visible table cells (without taking Shortcode parameters into account!).
959 foreach ( $table['data'] as $row_idx => $table_row ) {
960 if ( 0 === $table['visibility']['rows'][ $row_idx ] ) {
961 // Row is hidden, so don't search in it.
962 continue;
963 }
964 foreach ( $table_row as $col_idx => $table_cell ) {
965 if ( 0 === $table['visibility']['columns'][ $col_idx ] ) {
966 // Column is hidden, so don't search in it.
967 continue;
968 }
969 // @todo Cells are not evaluated here, so math formulas are searched.
970 if ( false !== stripos( $table_cell, $search_term ) ) {
971 // Found the search term in the cell content.
972 $query_result[ $search_term ][] = $table_id; // Add table ID to result list
973 // No need to continue searching this search term in this table.
974 continue 3;
975 }
976 }
977 }
978 }
979 }
980
981 // For all found table IDs for each search term, add additional OR statement to the SQL "WHERE" clause.
982
983 // If $_GET['exact'] is set, WordPress doesn't use % in SQL LIKE clauses.
984 $exact = get_query_var( 'exact' );
985 $n = ( empty( $exact ) ) ? '%' : '';
986 $search_sql = $wpdb->remove_placeholder_escape( $search_sql );
987 foreach ( $query_result as $search_term => $table_ids ) {
988 $search_term = esc_sql( $wpdb->esc_like( $search_term ) );
989 $old_or = "OR ({$wpdb->posts}.post_content LIKE '{$n}{$search_term}{$n}')"; // @phpstan-ignore-line (The esc_sql() call above returns a string, as a string is passed.)
990 $table_ids = implode( '|', $table_ids );
991 $regexp = '\\\\[' . TablePress::$shortcode . ' id=(["\\\']?)(' . $table_ids . ')([\]"\\\' /])'; // ' needs to be single escaped, [ double escaped (with \\) in mySQL
992 $new_or = $old_or . " OR ({$wpdb->posts}.post_content REGEXP '{$regexp}')";
993 $search_sql = str_replace( $old_or, $new_or, $search_sql );
994 }
995 $search_sql = $wpdb->add_placeholder_escape( $search_sql );
996
997 return $search_sql;
998 }
999
1000 /**
1001 * Callback function for rendering the tablepress/table block.
1002 *
1003 * @since 2.0.0
1004 *
1005 * @param array<string, string> $block_attributes List of attributes that where included in the block settings.
1006 * @return string Resulting HTML code for the table.
1007 */
1008 public function table_block_render_callback( array $block_attributes ): string {
1009 // Don't return anything if no table was selected.
1010 if ( '' === $block_attributes['id'] ) {
1011 return '';
1012 }
1013
1014 if ( '' !== trim( $block_attributes['parameters'] ) ) {
1015 $render_attributes = shortcode_parse_atts( $block_attributes['parameters'] );
1016 } else {
1017 $render_attributes = array();
1018 }
1019 $render_attributes['id'] = $block_attributes['id'];
1020
1021 return $this->shortcode_table( $render_attributes );
1022 }
1023
1024 } // class TablePress_Frontend_Controller
1025