PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
tablepress / controllers / controller-frontend.php

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

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