PluginProbe
TablePress – Tables in WordPress made easy / 3.2
TablePress – Tables in WordPress made easy v3.2
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 / classes / class-tablepress.php

class-tablepress.php in TablePress – Tables in WordPress made easy 3.2, at classes/class-tablepress.php

1,095 lines 42.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * TablePress Class
4 *
5 * @package TablePress
6 * @author Tobias Bäthge
7 * @since 1.0.0
8 */
9
10 // Prohibit direct script loading.
11 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
12
13 /**
14 * TablePress class
15 *
16 * @package TablePress
17 * @author Tobias Bäthge
18 * @since 1.0.0
19 */
20 abstract class TablePress {
21
22 /**
23 * TablePress version.
24 *
25 * Increases whenever a new plugin version is released.
26 *
27 * @since 1.0.0
28 * @const string
29 */
30 public const version = '3.2'; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
31
32 /**
33 * TablePress internal plugin version ("options scheme" version).
34 *
35 * Increases whenever the scheme for the plugin options changes, or on a plugin update.
36 *
37 * @since 1.0.0
38 * @const int
39 */
40 public const db_version = 113; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
41
42 /**
43 * TablePress "table scheme" (data format structure) version.
44 *
45 * Increases whenever the scheme for a $table changes,
46 * used to be able to update plugin options and table scheme independently.
47 *
48 * @since 1.0.0
49 * @const int
50 */
51 public const table_scheme_version = 3; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
52
53 /**
54 * Instance of the Options Model.
55 *
56 * @since 1.3.0
57 */
58 public static \TablePress_Options_Model $model_options;
59
60 /**
61 * Instance of the Table Model.
62 *
63 * @since 1.3.0
64 */
65 public static \TablePress_Table_Model $model_table;
66
67 /**
68 * Instance of the controller.
69 *
70 * @since 1.0.0
71 */
72 public static \TablePress_Frontend_Controller $controller;
73
74 /**
75 * Name of the Shortcode to show a TablePress table.
76 *
77 * Should only be modified through the filter hook 'tablepress_table_shortcode'.
78 *
79 * @since 1.0.0
80 */
81 public static string $shortcode = 'table';
82
83 /**
84 * Name of the Shortcode to show extra information of a TablePress table.
85 *
86 * Should only be modified through the filter hook 'tablepress_table_info_shortcode'.
87 *
88 * @since 1.0.0
89 */
90 public static string $shortcode_info = 'table-info';
91
92 /**
93 * List of TablePress premium modules.
94 *
95 * @since 2.1.0
96 * @var array<string, array{name: string, description: string, category: string, class: string, incompatible_classes: string[], minimum_plan: string, default_active: bool}>
97 */
98 public static array $modules = array();
99
100 /**
101 * Start-up TablePress (run on WordPress "init") and load the controller for the current state.
102 *
103 * @since 1.0.0
104 */
105 public static function run(): void {
106 /**
107 * Fires before TablePress is loaded.
108 *
109 * The `tablepress_loaded` action hook might be a better choice in most situations, as TablePress options will then be available.
110 *
111 * @since 1.0.0
112 */
113 do_action( 'tablepress_run' );
114
115 /**
116 * Filters the string that is used as the [table] Shortcode.
117 *
118 * @since 1.0.0
119 *
120 * @param string $shortcode The [table] Shortcode string.
121 */
122 self::$shortcode = apply_filters( 'tablepress_table_shortcode', self::$shortcode );
123 /**
124 * Filters the string that is used as the [table-info] Shortcode.
125 *
126 * @since 1.0.0
127 *
128 * @param string $shortcode_info The [table-info] Shortcode string.
129 */
130 self::$shortcode_info = apply_filters( 'tablepress_table_info_shortcode', self::$shortcode_info );
131
132 // Load modals for table and options, to be accessible from everywhere via `TablePress::$model_options` and `TablePress::$model_table`.
133 self::$model_options = self::load_model( 'options' );
134 self::$model_table = self::load_model( 'table' );
135
136 // Exit early, i.e. before a controller is loaded, if TablePress functionality is likely not needed.
137 $exit_early = false;
138 if ( ( isset( $_SERVER['SCRIPT_FILENAME'] ) && 'wp-login.php' === basename( $_SERVER['SCRIPT_FILENAME'] ) ) // Detect the WordPress Login screen.
139 || ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
140 || wp_doing_cron() ) {
141 $exit_early = true;
142 }
143 /**
144 * Filters whether TablePress should exit early, e.g. during wp-login.php, XML-RPC, and WP-Cron requests.
145 *
146 * @since 2.0.0
147 *
148 * @param bool $exit_early Whether TablePress should exit early.
149 */
150 if ( apply_filters( 'tablepress_exit_early', $exit_early ) ) {
151 return;
152 }
153
154 if ( is_admin() ) {
155 $controller = 'admin';
156 if ( wp_doing_ajax() ) {
157 $controller = 'admin_ajax';
158 }
159 self::load_controller( $controller );
160 }
161 // Load the frontend controller in all scenarios, so that Shortcode render functions are always available.
162 self::$controller = self::load_controller( 'frontend' );
163
164 // Add filters and actions for the integration into the WP WXR exporter and importer.
165 add_action( 'wp_import_insert_post', array( TablePress::$model_table, 'add_table_id_on_wp_import' ), 10, 4 ); // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
166 add_filter( 'wp_import_post_meta', array( TablePress::$model_table, 'prevent_table_id_post_meta_import_on_wp_import' ), 10, 3 ); // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
167 add_filter( 'wxr_export_skip_postmeta', array( TablePress::$model_table, 'add_table_id_to_wp_export' ), 10, 3 ); // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
168
169 /**
170 * Fires after TablePress is loaded.
171 *
172 * The `tablepress_run` action hook can be used if code has to run before TablePress is loaded.
173 *
174 * @since 2.0.0
175 */
176 do_action( 'tablepress_loaded' );
177 }
178
179 /**
180 * Load a file with require_once(), after running it through a filter.
181 *
182 * @since 1.0.0
183 *
184 * @param string $file Name of the PHP file.
185 * @param string $folder Name of the folder with the file.
186 */
187 public static function load_file( string $file, string $folder ): void {
188 $full_path = TABLEPRESS_ABSPATH . $folder . '/' . $file;
189 /**
190 * Filters the full path of a file that shall be loaded.
191 *
192 * @since 1.0.0
193 *
194 * @param string $full_path Full path of the file that shall be loaded.
195 * @param string $file File name of the file that shall be loaded.
196 * @param string $folder Folder name of the file that shall be loaded.
197 */
198 $full_path = apply_filters( 'tablepress_load_file_full_path', $full_path, $file, $folder );
199 if ( $full_path ) {
200 require_once $full_path;
201 }
202 }
203
204 /**
205 * Create a new instance of the $class_name, which is stored in $file in the $folder subfolder
206 * of the plugin's directory.
207 *
208 * @since 1.0.0
209 *
210 * @param string $class_name Name of the class.
211 * @param string $file Name of the PHP file with the class.
212 * @param string $folder Name of the folder with $class_name's $file.
213 * @param mixed[]|string|null $params Optional. Parameters that are passed to the constructor of $class_name.
214 * @return object Initialized instance of the class.
215 */
216 public static function load_class( string $class_name, string $file, string $folder, /* ?array|string */ $params = null ): object {
217 /**
218 * Filters name of the class that shall be loaded.
219 *
220 * @since 1.0.0
221 *
222 * @param string $class_name Name of the class that shall be loaded.
223 */
224 $class_name = apply_filters( 'tablepress_load_class_name', $class_name );
225 if ( ! class_exists( $class_name, false ) ) {
226 self::load_file( $file, $folder );
227 }
228 $the_class = new $class_name( $params );
229 return $the_class;
230 }
231
232 /**
233 * Create a new instance of the $model, which is stored in the "models" subfolder.
234 *
235 * @since 1.0.0
236 *
237 * @param string $model Name of the model.
238 * @return object Instance of the initialized model.
239 */
240 public static function load_model( string $model ): object {
241 // Model Base Class.
242 self::load_file( 'class-model.php', 'classes' );
243 // Make first letter uppercase for a better looking naming pattern.
244 $ucmodel = ucfirst( $model );
245 $the_model = self::load_class( "TablePress_{$ucmodel}_Model", "model-{$model}.php", 'models' );
246 return $the_model;
247 }
248
249 /**
250 * Create a new instance of the $view, which is stored in the "views" subfolder, and set it up with $data.
251 *
252 * @since 1.0.0
253 *
254 * @param string $view Name of the view to load.
255 * @param array<string, mixed> $data Optional. Parameters/PHP variables that shall be available to the view.
256 * @return object Instance of the initialized view, already set up, just needs to be rendered.
257 */
258 public static function load_view( string $view, array $data = array() ): object {
259 // View Base Class.
260 self::load_file( 'class-view.php', 'classes' );
261 // Make first letter uppercase for a better looking naming pattern.
262 $ucview = ucfirst( $view );
263 $the_view = self::load_class( "TablePress_{$ucview}_View", "view-{$view}.php", 'views' );
264 $the_view->setup( $view, $data );
265 return $the_view;
266 }
267
268 /**
269 * Create a new instance of the $controller, which is stored in the "controllers" subfolder.
270 *
271 * @since 1.0.0
272 *
273 * @param string $controller Name of the controller.
274 * @return object Instance of the initialized controller.
275 */
276 public static function load_controller( string $controller ): object {
277 // Controller Base Class.
278 self::load_file( 'class-controller.php', 'classes' );
279 // Make first letter uppercase for a better looking naming pattern.
280 $uccontroller = ucfirst( $controller );
281 $the_controller = self::load_class( "TablePress_{$uccontroller}_Controller", "controller-{$controller}.php", 'controllers' );
282 return $the_controller;
283 }
284
285 /**
286 * Generate the complete nonce string, from the nonce base, the action and an item, e.g. tablepress_delete_table_3.
287 *
288 * @since 1.0.0
289 *
290 * @param string $action Action for which the nonce is needed.
291 * @param string|false $item Optional. Item for which the action will be performed, like "table". false if no item should be used in the nonce.
292 * @return string The resulting nonce string.
293 */
294 public static function nonce( string $action, /* string|false */ $item = false ): string {
295 $nonce = "tablepress_{$action}";
296 if ( $item ) {
297 $nonce .= "_{$item}";
298 }
299 return $nonce;
300 }
301
302 /**
303 * Check whether a nonce string is valid.
304 *
305 * @since 1.0.0
306 *
307 * @param string $action Action for which the nonce should be checked.
308 * @param string|false $item Optional. Item for which the action should be performed, like "table". false if no item should be used in the nonce.
309 * @param string $query_arg Optional. Name of the nonce query string argument in $_POST.
310 * @param bool $ajax Whether the nonce comes from an AJAX request.
311 */
312 public static function check_nonce( string $action, /* string|false */ $item = false, string $query_arg = '_wpnonce', bool $ajax = false ): void {
313 $nonce_action = self::nonce( $action, $item );
314 if ( $ajax ) {
315 check_ajax_referer( $nonce_action, $query_arg );
316 } else {
317 check_admin_referer( $nonce_action, $query_arg );
318 }
319 }
320
321 /**
322 * Calculate the column index (number) of a column header string (example: A is 1, AA is 27, ...).
323 *
324 * For the opposite, @see number_to_letter().
325 *
326 * @since 1.0.0
327 *
328 * @param string $column Column string.
329 * @return int Column number, 1-based.
330 */
331 public static function letter_to_number( string $column ): int {
332 $column = (string) preg_replace( '/[^A-Za-z]/', '', $column );
333 $column = strtoupper( $column );
334 $count = strlen( $column );
335 $number = 0;
336 for ( $i = 0; $i < $count; $i++ ) {
337 $number += ( ord( $column[ $count - 1 - $i ] ) - 64 ) * 26 ** $i;
338 }
339 return $number;
340 }
341
342 /**
343 * "Calculate" the column header string of a column index (example: 2 is B, AB is 28, ...).
344 *
345 * For the opposite, @see letter_to_number().
346 *
347 * @since 1.0.0
348 *
349 * @param int $number Column number, 1-based.
350 * @return string Column string.
351 */
352 public static function number_to_letter( int $number ): string {
353 $column = '';
354 while ( $number > 0 ) {
355 $column = chr( 65 + ( ( $number - 1 ) % 26 ) ) . $column;
356 $number = intdiv( $number - 1, 26 );
357 }
358 return $column;
359 }
360
361 /**
362 * Get a nice looking date and time string from the mySQL format of datetime strings for output.
363 *
364 * @since 1.0.0
365 *
366 * @param string $datetime_string DateTime string, often in mySQL format..
367 * @param string $separator_or_format Optional. Separator between date and time, or format string.
368 * @return string Nice looking string with the date and time.
369 */
370 public static function format_datetime( string $datetime_string, string $separator_or_format = ' ' ): string {
371 $timezone = wp_timezone();
372 $datetime = date_create( $datetime_string, $timezone );
373 if ( false === $datetime ) {
374 return $datetime_string;
375 }
376 $timestamp = $datetime->getTimestamp();
377
378 switch ( $separator_or_format ) {
379 case ' ':
380 case '<br />':
381 case '<br/>':
382 case '<br>':
383 $date = wp_date( get_option( 'date_format' ), $timestamp, $timezone );
384 $time = wp_date( get_option( 'time_format' ), $timestamp, $timezone );
385 $output = "{$date}{$separator_or_format}{$time}";
386 break;
387 default:
388 $output = (string) wp_date( $separator_or_format, $timestamp, $timezone );
389 break;
390 }
391
392 return $output;
393 }
394
395 /**
396 * Get the name from a WP user ID (used to store information on last editor of a table).
397 *
398 * @since 1.0.0
399 *
400 * @param int $user_id WP user ID.
401 * @return string Nickname of the WP user with the $user_id.
402 */
403 public static function get_user_display_name( int $user_id ): string {
404 $user = get_userdata( $user_id );
405 /* translators: %s: Label for unknown user */
406 return $user->display_name ?? sprintf( '<em>%s</em>', __( 'unknown', 'tablepress' ) );
407 }
408
409 /**
410 * Sanitizes a CSS class to ensure it only contains valid characters.
411 *
412 * Strips the string down to A-Z, a-z, 0-9, :, _, -.
413 * This is an extension to WP's `sanitize_html_class()`, to also allow `:` which are used in some CSS frameworks.
414 *
415 * @since 1.11.0
416 *
417 * @param string $css_class The CSS class name to be sanitized.
418 * @return string The sanitized CSS class.
419 */
420 public static function sanitize_css_class( string $css_class ): string {
421 // Strip out any %-encoded octets.
422 $sanitized_css_class = (string) preg_replace( '|%[a-fA-F0-9][a-fA-F0-9]|', '', $css_class );
423 // Limit to A-Z, a-z, 0-9, ':', '_', and '-'.
424 $sanitized_css_class = (string) preg_replace( '/[^A-Za-z0-9:_-]/', '', $sanitized_css_class );
425 return $sanitized_css_class;
426 }
427
428 /**
429 * Extracts the top-level keys from a JavaScript object string.
430 *
431 * This function is used to extract the keys of the "Custom Commands" JavaScript object string, to check for overrides.
432 * It covers most cases, like normal object properties with and without quotes, shorthand properties, and shorthand methods,
433 * and also ignores single-line and multi-line comments.
434 * It does not cover all possible JavaScript syntax (like template literals, special characters, ...),
435 * but should be sufficient for the use case.
436 *
437 * @since 3.0.0
438 *
439 * @param string $js_object_string A JavaScript object as a string.
440 * @return string[] Array of top-level keys of the object.
441 */
442 public static function extract_keys_from_js_object_string( string $js_object_string ): array {
443 $object_keys = array();
444 $length = strlen( $js_object_string );
445 $depth = 0;
446 $key_expected = true;
447 $in_quotes = false;
448 $quote_char = '';
449 $in_function_declaration = false;
450 $in_single_line_comment = false;
451 $in_multi_line_comment = false;
452 $object_key = '';
453
454 for ( $i = 0; $i < $length; $i++ ) {
455 $char = $js_object_string[ $i ];
456
457 // Skip parsing single-line comments.
458 if ( $in_single_line_comment ) {
459 if ( "\n" === $char ) {
460 $in_single_line_comment = false;
461 }
462 continue;
463 } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
464 if ( '/' === $char && $i + 1 < $length && '/' === $js_object_string[ $i + 1 ] ) {
465 $in_single_line_comment = true;
466 ++$i; // Skip the second '/'.
467 continue;
468 }
469 }
470
471 // Skip parsing multi-line comments.
472 if ( $in_multi_line_comment ) {
473 if ( '*' === $char && $i + 1 < $length && '/' === $js_object_string[ $i + 1 ] ) {
474 $in_multi_line_comment = false;
475 ++$i; // Skip the '/' that ends the multi-line comment.
476 }
477 continue;
478 } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
479 if ( '/' === $char && $i + 1 < $length && '*' === $js_object_string[ $i + 1 ] ) {
480 $in_multi_line_comment = true;
481 ++$i; // Skip the '*'.
482 continue;
483 }
484 }
485
486 // Skip parsing while inside a quoted string.
487 if ( $in_quotes ) {
488 if ( $quote_char === $char ) {
489 $in_quotes = false;
490 }
491 continue;
492 } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
493 if ( '"' === $char || "'" === $char ) {
494 $in_quotes = true;
495 $quote_char = $char;
496 continue;
497 }
498 }
499
500 /*
501 * Skip parsing while inside a `function abc( ... )` declaration string.
502 * The `$key_expected` check limits search the "function" string to object values.
503 * The check for the plain `f` reduces expensive `substr()` calls.
504 */
505 if ( ! $key_expected ) {
506 if ( $in_function_declaration ) {
507 if ( ')' === $char ) {
508 $in_function_declaration = false;
509 }
510 continue;
511 } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
512 if ( 'f' === $char && 'function' === substr( $js_object_string, $i, 8 ) ) {
513 $in_function_declaration = true;
514 $i += 7; // Skip the rest of the "function" string.
515 continue;
516 }
517 }
518 }
519
520 // Handle object depth, so that most parsing can be limited to the top level.
521 if ( '{' === $char || '[' === $char ) {
522 ++$depth;
523 }
524
525 // Extract only keys at the top level.
526 if ( 1 === $depth ) {
527 if ( $key_expected ) {
528 if ( ':' === $char ) {
529 // Check for normal keys, with value after :.
530
531 // Go backwards to find the start of the key.
532 $j = $i - 1;
533 while ( $j >= 0 && preg_match( '/\s/', $js_object_string[ $j ] ) ) {
534 --$j;
535 }
536 $key_end = $j; // Position of the last character of the key (potentially with quote).
537 if ( '"' === $js_object_string[ $j ] || "'" === $js_object_string[ $j ] ) {
538 // Quoted key.
539 $quote_char = $js_object_string[ $j ];
540 --$j;
541 while ( $j >= 0 && $quote_char !== $js_object_string[ $j ] ) {
542 --$j;
543 }
544 $key_start = $j + 1;
545 } else {
546 // Unquoted key.
547 while ( $j >= 0 && preg_match( '/[\w]/', $js_object_string[ $j ] ) ) {
548 --$j;
549 }
550 $key_start = $j + 1;
551 }
552 $object_key = substr( $js_object_string, $key_start, $key_end - $key_start + 1 );
553 $object_key = trim( $object_key, "\"'" );
554 if ( '' !== $object_key && ! in_array( $object_key, $object_keys, true ) ) {
555 $object_keys[] = $object_key;
556 }
557 $key_expected = false;
558 } elseif ( ( ',' === $char || '}' === $char ) ) { // The `}` case is for the last key.
559 // Check for shorthand properties (which must be unquoted).
560
561 // Go backwards to find the start of the shorthand key.
562 $j = $i - 1;
563 while ( $j >= 0 && preg_match( '/\s/', $js_object_string[ $j ] ) ) {
564 --$j;
565 }
566 $key_end = $j; // Position of the last character of the key (without a quote).
567 while ( $j >= 0 && preg_match( '/[\w]/', $js_object_string[ $j ] ) ) {
568 --$j;
569 }
570 $key_start = $j + 1;
571 $object_key = substr( $js_object_string, $key_start, $key_end - $key_start + 1 );
572 if ( '' !== $object_key && ! in_array( $object_key, $object_keys, true ) ) {
573 $object_keys[] = $object_key;
574 }
575 } elseif ( '(' === $char ) {
576 // Detect shorthand method definitions.
577
578 // Go back to find the start of the method name.
579 $j = $i - 1;
580 while ( $j >= 0 && preg_match( '/\s/', $js_object_string[ $j ] ) ) {
581 --$j;
582 }
583 $key_end = $j;
584 while ( $j >= 0 && preg_match( '/[\w]/', $js_object_string[ $j ] ) ) {
585 --$j;
586 }
587 $key_start = $j + 1;
588 $object_key = substr( $js_object_string, $key_start, $key_end - $key_start + 1 );
589 if ( '' !== $object_key && ! in_array( $object_key, $object_keys, true ) ) {
590 $object_keys[] = $object_key;
591 }
592 }
593 }
594
595 // Reset the "key expected" flag after a comma or closing brace.
596 if ( ',' === $char || '}' === $char ) {
597 $key_expected = true;
598 }
599 }
600
601 // Handle object depth.
602 if ( '}' === $char || ']' === $char ) {
603 --$depth;
604 }
605 }
606
607 return $object_keys;
608 }
609
610 /**
611 * Converts old DataTables 1.x CSS classes and parameters to the DataTables 2 variants.
612 *
613 * This function is used to modernize "Custom CSS" and "Custom Commands" for compatibility with DataTables 2.x.
614 * It probably does not catch all possible cases.
615 *
616 * @since 3.0.0
617 *
618 * @param string $code Code that contains DataTables 1.x CSS classes and parameters.
619 * @return string Updated code with DataTables 2.x CSS classes and parameters.
620 */
621 public static function convert_datatables_api_data( string $code ): string {
622 /**
623 * Mappings for DataTables 1.x CSS class or parameter to DataTables 2 variants.
624 * As this array is used in `strtr()`, it's pre-sorted for descending string length of the array keys.
625 */
626 static $datatables_api_data_mappings = array(
627 // CSS classes.
628 '.tablepress thead .sorting:hover' => '.tablepress thead .dt-orderable-asc:hover,.tablepress thead .dt-orderable-desc:hover',
629 '.tablepress thead .sorting_desc' => '.tablepress thead .dt-ordering-desc',
630 '.dataTables_filter label input' => '.dt-container .dt-search input',
631 '.tablepress thead .sorting_asc' => '.tablepress thead .dt-ordering-asc',
632 '.dataTables_scrollFootInner' => '.dt-scroll-footInner',
633 '.dataTables_scrollHeadInner' => '.dt-scroll-headInner',
634 '.tablepress thead .sorting' => '.tablepress thead .dt-orderable-asc,.tablepress thead .dt-orderable-desc',
635 '.dataTables_processing' => '.dt-processing',
636 '.dataTables_scrollBody' => '.dt-scroll-body',
637 '.dataTables_scrollFoot' => '.dt-scroll-foot',
638 '.dataTables_scrollHead' => '.dt-scroll-head',
639 '.dataTables_paginate' => '.dt-paging',
640 '.tablepress .even td' => '.tablepress>:where(tbody.row-striping)>:nth-child(odd)>*',
641 '.dataTables_wrapper' => '.dt-container',
642 '.tablepress .odd td' => '.tablepress>:where(tbody.row-striping)>:nth-child(even)>*',
643 '.dataTables_filter' => '.dt-search',
644 '.dataTables_length' => '.dt-length',
645 '.dataTables_scroll' => '.dt-scroll',
646 '.dataTables_empty' => '.dt-empty',
647 '.dataTables_info' => '.dt-info',
648 '.paginate_button' => '.dt-paging-button',
649 // DataTables API functions.
650 '$.fn.dataTable.' => 'DataTable.',
651 );
652 $code = strtr( $code, $datatables_api_data_mappings );
653
654 // HTML ID mappings, which were removed.
655 if ( str_contains( $code, '#tablepress-' ) ) {
656 $code = (string) preg_replace(
657 array(
658 '/#tablepress-([A-Za-z1-9_-]|[A-Za-z0-9_-]{2,})_paginate/',
659 '/#tablepress-([A-Za-z1-9_-]|[A-Za-z0-9_-]{2,})_filter/',
660 '/#tablepress-([A-Za-z1-9_-]|[A-Za-z0-9_-]{2,})_length/',
661 '/#tablepress-([A-Za-z1-9_-]|[A-Za-z0-9_-]{2,})_info/',
662 ),
663 array(
664 '#tablepress-$1_wrapper .dt-paging',
665 '#tablepress-$1_wrapper .dt-search',
666 '#tablepress-$1_wrapper .dt-length',
667 '#tablepress-$1_wrapper .dt-info',
668 ),
669 $code,
670 );
671 }
672
673 return $code;
674 }
675
676 /**
677 * Retrieves all information of a WP_Error object as a string.
678 *
679 * @since 1.4.0
680 *
681 * @param WP_Error $wp_error A WP_Error object.
682 * @return string All error codes, messages, and data of the WP_Error.
683 */
684 public static function get_wp_error_string( WP_Error $wp_error ): string {
685 $error_strings = array();
686 $error_codes = $wp_error->get_error_codes();
687 // Reverse order to get latest errors first.
688 $error_codes = array_reverse( $error_codes );
689 foreach ( $error_codes as $error_code ) {
690 $error_strings[ $error_code ] = $error_code;
691 $error_messages = $wp_error->get_error_messages( $error_code );
692 $error_messages = implode( ', ', $error_messages );
693 if ( ! empty( $error_messages ) ) {
694 $error_strings[ $error_code ] .= " ({$error_messages})";
695 }
696 $error_data = $wp_error->get_error_data( $error_code );
697 if ( is_string( $error_data ) ) {
698 $error_strings[ $error_code ] .= " [{$error_data}]";
699 } elseif ( is_array( $error_data ) ) {
700 foreach ( $error_data as $key => $value ) {
701 $error_data[ $key ] = "{$key}: {$value}";
702 }
703 $error_data = implode( ', ', $error_data );
704 $error_strings[ $error_code ] .= " [{$error_data}]";
705 }
706 }
707 return implode( ";\n", $error_strings );
708 }
709
710 /**
711 * Generate the action URL, to be used as a link within the plugin (e.g. in the submenu navigation or List of Tables).
712 *
713 * @since 1.0.0
714 *
715 * @param array<string, mixed> $params Optional. Parameters to form the query string of the URL.
716 * @param bool $add_nonce Optional. Whether the URL shall be nonced by WordPress.
717 * @param string $target Optional. Target File, e.g. "admin-post.php" for POST requests.
718 * @return string The URL for the given parameters (already run through esc_url() with $add_nonce === true!).
719 */
720 public static function url( array $params = array(), bool $add_nonce = false, string $target = '' ): string {
721 // Default action is "list", if no action given.
722 if ( ! isset( $params['action'] ) ) {
723 $params['action'] = 'list';
724 }
725 $nonce_action = $params['action'];
726
727 if ( '' !== $target ) {
728 $params['action'] = "tablepress_{$params['action']}";
729 } else {
730 $params['page'] = 'tablepress';
731 // Top-level parent page needs special treatment for better action strings.
732 if ( self::$controller->is_top_level_page ) {
733 $target = 'admin.php';
734 if ( ! in_array( $params['action'], array( 'list', 'edit' ), true ) ) {
735 $params['page'] = "tablepress_{$params['action']}";
736 }
737 if ( ! in_array( $params['action'], array( 'edit' ), true ) ) {
738 $params['action'] = false;
739 }
740 } else {
741 $target = self::$controller->parent_page;
742 }
743 }
744
745 // $default_params also determines the order of the values in the query string.
746 $default_params = array(
747 'page' => false,
748 'action' => false,
749 'item' => false,
750 );
751 $params = array_merge( $default_params, $params );
752
753 $url = add_query_arg( $params, admin_url( $target ) );
754 if ( $add_nonce ) {
755 $url = wp_nonce_url( $url, self::nonce( $nonce_action, $params['item'] ) ); // wp_nonce_url() does esc_html().
756 }
757 return $url;
758 }
759
760 /**
761 * Create a redirect URL from the $target_parameters and redirect the user.
762 *
763 * @since 1.0.0
764 *
765 * @param array<string, mixed> $params Optional. Parameters from which the target URL is constructed.
766 * @param bool $add_nonce Optional. Whether the URL shall be nonced by WordPress.
767 */
768 public static function redirect( array $params = array(), bool $add_nonce = false ): void {
769 $redirect = self::url( $params );
770 if ( $add_nonce ) {
771 if ( ! isset( $params['item'] ) ) {
772 $params['item'] = false;
773 }
774 // Don't use wp_nonce_url(), as that uses esc_html().
775 $redirect = add_query_arg( '_wpnonce', wp_create_nonce( self::nonce( $params['action'], $params['item'] ) ), $redirect );
776 }
777 wp_redirect( $redirect );
778 exit;
779 }
780
781 /**
782 * Determines the editor that the site uses, so that certain text and input fields referring to Shortcodes can be displayed or not.
783 *
784 * @since 3.1.0
785 *
786 * @return string The editor that the site uses, either "block", "elementor", or "other".
787 */
788 public static function site_used_editor(): string {
789 if ( is_plugin_active( 'elementor/elementor.php' ) ) {
790 return 'elementor';
791 }
792
793 // Checking for Elementor is not needed anymore in this condition.
794 $site_uses_block_editor = use_block_editor_for_post_type( 'post' )
795 && ! is_plugin_active( 'classic-editor/classic-editor.php' )
796 && ! is_plugin_active( 'classic-editor-addon/classic-editor-addon.php' )
797 && ! is_plugin_active( 'siteorigin-panels/siteorigin-panels.php' )
798 && ! is_plugin_active( 'beaver-builder-lite-version/fl-builder.php' );
799 /**
800 * Filters the outcome of the check whether the site uses the block editor.
801 *
802 * This can be used when certain conditions (e.g. new site builders) are not (yet) accounted for.
803 *
804 * @since 2.0.1
805 *
806 * @param bool $site_uses_block_editor True if the site uses the block editor, false otherwise.
807 */
808 $site_uses_block_editor = (bool) apply_filters( 'tablepress_site_uses_block_editor', $site_uses_block_editor );
809 if ( $site_uses_block_editor ) {
810 return 'block';
811 }
812
813 return 'other';
814 }
815
816 /**
817 * Initializes the list of TablePress premium modules.
818 *
819 * @since 2.1.0
820 */
821 public static function init_modules(): void {
822 if ( ! empty( self::$modules ) ) {
823 return;
824 }
825
826 self::$modules = array(
827 'advanced-access-rights' => array(
828 'name' => __( 'Advanced Access Rights', 'tablepress' ),
829 'description' => __( 'Restrict access to individual tables for individual users.', 'tablepress' ),
830 'category' => 'backend',
831 'class' => 'TablePress_Module_Advanced_Access_Rights',
832 'incompatible_classes' => array( 'TablePress_Advanced_Access_Rights_Controller' ),
833 'minimum_plan' => 'max',
834 'default_active' => false,
835 ),
836 'automatic-periodic-table-import' => array(
837 'name' => __( 'Automatic Periodic Table Import', 'tablepress' ),
838 'description' => __( 'Periodically update tables from a configured import source.', 'tablepress' ),
839 'category' => 'backend',
840 'class' => 'TablePress_Module_Automatic_Periodic_Table_Import',
841 'incompatible_classes' => array( 'TablePress_Table_Auto_Update' ),
842 'minimum_plan' => 'max',
843 'default_active' => true,
844 ),
845 'automatic-table-export' => array(
846 'name' => __( 'Automatic Table Export', 'tablepress' ),
847 'description' => __( 'Export and save tables to files on the server after they were modified.', 'tablepress' ),
848 'category' => 'backend',
849 'class' => 'TablePress_Module_Automatic_Table_Export',
850 'incompatible_classes' => array(),
851 'minimum_plan' => 'pro',
852 'default_active' => false,
853 ),
854 'cell-highlighting' => array(
855 'name' => __( 'Cell Highlighting', 'tablepress' ),
856 'description' => __( 'Add CSS classes to cells for highlighting based on their content.', 'tablepress' ),
857 'category' => 'frontend',
858 'class' => 'TablePress_Module_Cell_Highlighting',
859 'incompatible_classes' => array( 'TablePress_Cell_Highlighting' ),
860 'minimum_plan' => 'pro',
861 'default_active' => false,
862 ),
863 'column-order' => array(
864 'name' => __( 'Column Order', 'tablepress' ),
865 'description' => __( 'Order the columns in different ways when a table is shown.', 'tablepress' ),
866 'category' => 'data-management',
867 'class' => 'TablePress_Module_Column_Order',
868 'incompatible_classes' => array( 'TablePress_Column_Order' ),
869 'minimum_plan' => 'pro',
870 'default_active' => false,
871 ),
872 'datatables-advanced-loading' => array(
873 'name' => __( 'Advanced Loading', 'tablepress' ),
874 'description' => __( 'Load the table data from a JSON array for faster loading.', 'tablepress' ),
875 'category' => 'backend',
876 'class' => 'TablePress_Module_DataTables_Advanced_Loading',
877 'incompatible_classes' => array( 'TablePress_DataTables_Advanced_Loading' ),
878 'minimum_plan' => 'max',
879 'default_active' => false,
880 ),
881 'datatables-alphabetsearch' => array(
882 'name' => __( 'Alphabet Search', 'tablepress' ),
883 'description' => __( 'Show Alphabet buttons above the table to filter rows by their first letter.', 'tablepress' ),
884 'category' => 'search-filter',
885 'class' => 'TablePress_Module_DataTables_Alphabetsearch',
886 'incompatible_classes' => array(),
887 'minimum_plan' => 'pro',
888 'default_active' => false,
889 ),
890 'datatables-auto-filter' => array(
891 'name' => __( 'Automatic Filter', 'tablepress' ),
892 'description' => __( 'Pre-filter a table when it is shown.', 'tablepress' ),
893 'category' => 'search-filter',
894 'class' => 'TablePress_Module_DataTables_Auto_Filter',
895 'incompatible_classes' => array( 'TablePress_DataTables_Auto_Filter' ),
896 'minimum_plan' => 'pro',
897 'default_active' => false,
898 ),
899 'datatables-buttons' => array(
900 'name' => __( 'User Action Buttons', 'tablepress' ),
901 'description' => __( 'Add buttons for downloading, copying, printing, and changing column visibility of tables.', 'tablepress' ),
902 'category' => 'frontend',
903 'class' => 'TablePress_Module_DataTables_Buttons',
904 'incompatible_classes' => array( 'TablePress_DataTables_Buttons' ),
905 'minimum_plan' => 'pro',
906 'default_active' => true,
907 ),
908 'datatables-columnfilterwidgets' => array(
909 'name' => __( 'Column Filter Dropdowns', 'tablepress' ),
910 'description' => __( 'Add a search dropdown for each column above the table.', 'tablepress' ),
911 'category' => 'search-filter',
912 'class' => 'TablePress_Module_DataTables_ColumnFilterWidgets',
913 'incompatible_classes' => array(),
914 'minimum_plan' => 'pro',
915 'default_active' => true,
916 ),
917 'datatables-column-filter' => array(
918 'name' => __( 'Individual Column Filtering', 'tablepress' ),
919 'description' => __( 'Add a search field for each column to the table head or foot row.', 'tablepress' ),
920 'category' => 'search-filter',
921 'class' => 'TablePress_Module_DataTables_Column_Filter',
922 'incompatible_classes' => array(),
923 'minimum_plan' => 'pro',
924 'default_active' => false,
925 ),
926 'datatables-counter-column' => array(
927 'name' => __( 'Index Column', 'tablepress' ),
928 'description' => __( 'Make the first column an index or counter column with the row position.', 'tablepress' ),
929 'category' => 'frontend',
930 'class' => 'TablePress_Module_DataTables_Counter_Column',
931 'incompatible_classes' => array(),
932 'minimum_plan' => 'pro',
933 'default_active' => false,
934 ),
935 'datatables-fixedheader-fixedcolumns' => array(
936 'name' => __( 'Fixed Rows and Columns', 'tablepress' ),
937 'description' => __( 'Fix the header and footer row and the first and last column when scrolling the table.', 'tablepress' ),
938 'category' => 'frontend',
939 'class' => 'TablePress_Module_DataTables_FixedHeader_FixedColumns',
940 'incompatible_classes' => array(
941 'TablePress_DataTables_FixedHeader',
942 'TablePress_DataTables_FixedColumns',
943 ),
944 'minimum_plan' => 'pro',
945 'default_active' => true,
946 ),
947 'datatables-layout' => array(
948 'name' => __( 'Table Layout', 'tablepress' ),
949 'description' => __( 'Customize the layout and position of features around a table.', 'tablepress' ),
950 'category' => 'frontend',
951 'class' => 'TablePress_Module_DataTables_Layout',
952 'incompatible_classes' => array(),
953 'minimum_plan' => 'pro',
954 'default_active' => true,
955 ),
956 'datatables-fuzzysearch' => array(
957 'name' => __( 'Fuzzy Search', 'tablepress' ),
958 'description' => __( 'Let the search account for spelling mistakes and typos and find similar matches.', 'tablepress' ),
959 'category' => 'search-filter',
960 'class' => 'TablePress_Module_DataTables_FuzzySearch',
961 'incompatible_classes' => array(),
962 'minimum_plan' => 'max',
963 'default_active' => false,
964 ),
965 'datatables-inverted-filter' => array(
966 'name' => __( 'Inverted Filtering', 'tablepress' ),
967 'description' => __( 'Turn the filtering into a search and hide the table if no search term is entered.', 'tablepress' ),
968 'category' => 'search-filter',
969 'class' => 'TablePress_Module_DataTables_Inverted_Filter',
970 'incompatible_classes' => array(),
971 'minimum_plan' => 'max',
972 'default_active' => false,
973 ),
974 'datatables-pagination' => array(
975 'name' => __( 'Advanced Pagination Settings', 'tablepress' ),
976 'description' => __( 'Customize the pagination settings of the table.', 'tablepress' ),
977 'category' => 'frontend',
978 'class' => 'TablePress_Module_DataTables_Pagination',
979 'incompatible_classes' => array(),
980 'minimum_plan' => 'pro',
981 'default_active' => false,
982 ),
983 'datatables-rowgroup' => array(
984 'name' => __( 'Row Grouping', 'tablepress' ),
985 'description' => __( 'Group table rows by a common keyword, category, or title.', 'tablepress' ),
986 'category' => 'frontend',
987 'class' => 'TablePress_Module_DataTables_RowGroup',
988 'incompatible_classes' => array( 'TablePress_DataTables_RowGroup' ),
989 'minimum_plan' => 'pro',
990 'default_active' => false,
991 ),
992 'datatables-searchbuilder' => array(
993 'name' => __( 'Custom Search Builder', 'tablepress' ),
994 'description' => __( 'Show a search builder interface for filtering from groups and using conditions.', 'tablepress' ),
995 'category' => 'search-filter',
996 'class' => 'TablePress_Module_DataTables_SearchBuilder',
997 'incompatible_classes' => array(),
998 'minimum_plan' => 'max',
999 'default_active' => false,
1000 ),
1001 'datatables-searchhighlight' => array(
1002 'name' => __( 'Search Highlighting', 'tablepress' ),
1003 'description' => __( 'Highlight found search terms in the table.', 'tablepress' ),
1004 'category' => 'search-filter',
1005 'class' => 'TablePress_Module_DataTables_SearchHighlight',
1006 'incompatible_classes' => array(),
1007 'minimum_plan' => 'pro',
1008 'default_active' => false,
1009 ),
1010 'datatables-searchpanes' => array(
1011 'name' => __( 'Search Panes', 'tablepress' ),
1012 'description' => __( 'Show panes for filtering the columns.', 'tablepress' ),
1013 'category' => 'search-filter',
1014 'class' => 'TablePress_Module_DataTables_SearchPanes',
1015 'incompatible_classes' => array(),
1016 'minimum_plan' => 'pro',
1017 'default_active' => false,
1018 ),
1019 'datatables-serverside-processing' => array(
1020 'name' => __( 'Server-side Processing', 'tablepress' ),
1021 'description' => __( 'Process sorting, filtering, and pagination on the server for faster loading of large tables.', 'tablepress' ),
1022 'category' => 'backend',
1023 'class' => 'TablePress_Module_DataTables_ServerSide_Processing',
1024 'incompatible_classes' => array(),
1025 'minimum_plan' => 'max',
1026 'default_active' => true,
1027 ),
1028 'default-style-customizer' => array(
1029 'name' => __( 'Default Style Customizer', 'tablepress' ),
1030 'description' => __( 'Change the default styling of your tables in the visual style customizer.', 'tablepress' ),
1031 'category' => 'frontend',
1032 'class' => 'TablePress_Module_Default_Style_Customizer',
1033 'incompatible_classes' => array(),
1034 'minimum_plan' => 'pro',
1035 'default_active' => true,
1036 ),
1037 'email-notifications' => array(
1038 'name' => __( 'Email Notifications', 'tablepress' ),
1039 'description' => __( 'Get email notifications when certain actions are performed on tables.', 'tablepress' ),
1040 'category' => 'backend',
1041 'class' => 'TablePress_Module_Email_Notifications',
1042 'incompatible_classes' => array(),
1043 'minimum_plan' => 'max',
1044 'default_active' => false,
1045 ),
1046 'responsive-tables' => array(
1047 'name' => __( 'Responsive Tables', 'tablepress' ),
1048 'description' => __( 'Make your tables look good on different screen sizes.', 'tablepress' ),
1049 'category' => 'frontend',
1050 'class' => 'TablePress_Module_Responsive_Tables',
1051 'incompatible_classes' => array( 'TablePress_Responsive_Tables' ),
1052 'minimum_plan' => 'pro',
1053 'default_active' => true,
1054 ),
1055 'rest-api' => array(
1056 'name' => __( 'REST API', 'tablepress' ),
1057 'description' => __( 'Read table data via the WordPress REST API, e.g. in external apps.', 'tablepress' ),
1058 'category' => 'backend',
1059 'class' => 'TablePress_Module_REST_API',
1060 'incompatible_classes' => array( 'TablePress_REST_API_Controller' ),
1061 'minimum_plan' => 'max',
1062 'default_active' => false,
1063 ),
1064 'row-filtering' => array(
1065 'name' => __( 'Row Filtering', 'tablepress' ),
1066 'description' => __( 'Show only table rows that contain defined keywords.', 'tablepress' ),
1067 'category' => 'data-management',
1068 'class' => 'TablePress_Module_Row_Filtering',
1069 'incompatible_classes' => array( 'TablePress_Row_Filter' ),
1070 'minimum_plan' => 'pro',
1071 'default_active' => true,
1072 ),
1073 'row-highlighting' => array(
1074 'name' => __( 'Row Highlighting', 'tablepress' ),
1075 'description' => __( 'Add CSS classes to rows for highlighting based on their content.', 'tablepress' ),
1076 'category' => 'frontend',
1077 'class' => 'TablePress_Module_Row_Highlighting',
1078 'incompatible_classes' => array( 'TablePress_Row_Highlighting' ),
1079 'minimum_plan' => 'pro',
1080 'default_active' => false,
1081 ),
1082 'row-order' => array(
1083 'name' => __( 'Row Order', 'tablepress' ),
1084 'description' => __( 'Order the rows in different ways when a table is shown.', 'tablepress' ),
1085 'category' => 'data-management',
1086 'class' => 'TablePress_Module_Row_Order',
1087 'incompatible_classes' => array( 'TablePress_Row_Order' ),
1088 'minimum_plan' => 'pro',
1089 'default_active' => false,
1090 ),
1091 );
1092 }
1093
1094 } // class TablePress
1095