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
← All changes | classes/class-tablepress.php +556 -205 2.2.1 → 3.4 View file →
@@ -6,8 +6,10 @@
6 6 * @author Tobias Bäthge
7 7 * @since 1.0.0
8 8 */
9 9
10 +declare(strict_types=1);
11 +
10 12 // Prohibit direct script loading.
11 13 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
12 14
13 15 /**
@@ -26,9 +28,9 @@
26 28 *
27 29 * @since 1.0.0
28 30 * @const string
29 31 */
30 - public const version = '2.2.1'; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
32 + public const version = '3.4'; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
31 33
32 34 /**
33 35 * TablePress internal plugin version ("options scheme" version).
34 36 *
@@ -36,9 +38,9 @@
36 38 *
37 39 * @since 1.0.0
38 40 * @const int
39 41 */
40 - public const db_version = 67; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
42 + public const db_version = 133; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
41 43
42 44 /**
43 45 * TablePress "table scheme" (data format structure) version.
44 46 *
@@ -53,27 +55,24 @@
53 55 /**
54 56 * Instance of the Options Model.
55 57 *
56 58 * @since 1.3.0
57 - * @var TablePress_Options_Model
58 59 */
59 - public static $model_options;
60 + public static \TablePress_Options_Model $model_options;
60 61
61 62 /**
62 63 * Instance of the Table Model.
63 64 *
64 65 * @since 1.3.0
65 - * @var TablePress_Table_Model
66 66 */
67 - public static $model_table;
67 + public static \TablePress_Table_Model $model_table;
68 68
69 69 /**
70 70 * Instance of the controller.
71 71 *
72 72 * @since 1.0.0
73 - * @var TablePress_*_Controller
74 73 */
75 - public static $controller;
74 + public static \TablePress_Frontend_Controller $controller;
76 75
77 76 /**
78 77 * Name of the Shortcode to show a TablePress table.
79 78 *
@@ -79,11 +78,11 @@
79 78 *
80 79 * Should only be modified through the filter hook 'tablepress_table_shortcode'.
81 80 *
82 81 * @since 1.0.0
83 - * @var string
82 + * @var non-empty-string
84 83 */
85 - public static $shortcode = 'table';
84 + public static string $shortcode = 'table';
86 85
87 86 /**
88 87 * Name of the Shortcode to show extra information of a TablePress table.
89 88 *
@@ -89,19 +88,19 @@
89 88 *
90 89 * Should only be modified through the filter hook 'tablepress_table_info_shortcode'.
91 90 *
92 91 * @since 1.0.0
93 - * @var string
92 + * @var non-empty-string
94 93 */
95 - public static $shortcode_info = 'table-info';
94 + public static string $shortcode_info = 'table-info';
96 95
97 96 /**
98 97 * List of TablePress premium modules.
99 98 *
100 99 * @since 2.1.0
101 - * @var array<string, array{name: string, description: string, category: string, class: string, incompatible_classes: string[], minimum_plan: string, default_active: bool}>
100 + * @var array<string, array<string, mixed>> $modules Array with module slugs as keys and module data as values.
102 101 */
103 - public static $modules = array();
102 + public static array $modules = array();
104 103
105 104 /**
106 105 * Start-up TablePress (run on WordPress "init") and load the controller for the current state.
107 106 *
@@ -121,9 +120,9 @@
121 120 * Filters the string that is used as the [table] Shortcode.
122 121 *
123 122 * @since 1.0.0
124 123 *
125 - * @param string $shortcode The [table] Shortcode string.
124 + * @param non-empty-string $shortcode The [table] Shortcode string.
126 125 */
127 126 self::$shortcode = apply_filters( 'tablepress_table_shortcode', self::$shortcode );
128 127 /**
129 128 * Filters the string that is used as the [table-info] Shortcode.
@@ -129,9 +128,9 @@
129 128 * Filters the string that is used as the [table-info] Shortcode.
130 129 *
131 130 * @since 1.0.0
132 131 *
133 - * @param string $shortcode_info The [table-info] Shortcode string.
132 + * @param non-empty-string $shortcode_info The [table-info] Shortcode string.
134 133 */
135 134 self::$shortcode_info = apply_filters( 'tablepress_table_info_shortcode', self::$shortcode_info );
136 135
137 136 // Load modals for table and options, to be accessible from everywhere via `TablePress::$model_options` and `TablePress::$model_table`.
@@ -158,9 +157,9 @@
158 157
159 158 if ( is_admin() ) {
160 159 $controller = 'admin';
161 160 if ( wp_doing_ajax() ) {
162 - $controller .= '_ajax';
161 + $controller = 'admin_ajax';
163 162 }
164 163 self::load_controller( $controller );
165 164 }
166 165 // Load the frontend controller in all scenarios, so that Shortcode render functions are always available.
@@ -165,8 +164,13 @@
165 164 }
166 165 // Load the frontend controller in all scenarios, so that Shortcode render functions are always available.
167 166 self::$controller = self::load_controller( 'frontend' );
168 167
168 + // Add filters and actions for the integration into the WP WXR exporter and importer.
169 + add_action( 'wp_import_insert_post', array( TablePress::$model_table, 'add_table_id_on_wp_import' ), 10, 4 ); // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
170 + 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
171 + add_filter( 'wxr_export_skip_postmeta', array( TablePress::$model_table, 'add_table_id_to_wp_export' ), 10, 3 ); // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
172 +
169 173 /**
170 174 * Fires after TablePress is loaded.
171 175 *
172 176 * The `tablepress_run` action hook can be used if code has to run before TablePress is loaded.
@@ -328,13 +332,14 @@
328 332 * @param string $column Column string.
329 333 * @return int Column number, 1-based.
330 334 */
331 335 public static function letter_to_number( string $column ): int {
336 + $column = (string) preg_replace( '/[^A-Za-z]/', '', $column );
332 337 $column = strtoupper( $column );
333 338 $count = strlen( $column );
334 339 $number = 0;
335 340 for ( $i = 0; $i < $count; $i++ ) {
336 - $number += ( ord( $column[ $count - 1 - $i ] ) - 64 ) * pow( 26, $i );
341 + $number += ( ord( $column[ $count - 1 - $i ] ) - 64 ) * 26 ** $i;
337 342 }
338 343 return $number;
339 344 }
340 345
@@ -357,8 +362,102 @@
357 362 return $column;
358 363 }
359 364
360 365 /**
366 + * Converts a range string (e.g., "1-5" or "A-E") to an array of numbers.
367 + *
368 + * @since 3.4.0
369 + *
370 + * @param string $value The range string.
371 + * @param int $min_number The minimum allowed number for the range, usually 0 or 1.
372 + * @param int $max_number The maximum allowed number for the range, usually the number of rows or columns in the table.
373 + * @return int[] The array of numbers in the range.
374 + */
375 + public static function convert_range_to_array( string $value, int $min_number, int $max_number ): array {
376 + $parts = explode( '-', $value );
377 +
378 + // Ignore invalid ranges, e.g., "1-5-10" or "A-".
379 + if ( count( $parts ) !== 2 || '' === $parts[0] || '' === $parts[1] ) {
380 + return array();
381 + }
382 +
383 + $start = trim( $parts[0] );
384 + if ( ! is_numeric( $start ) ) {
385 + $start = self::letter_to_number( $start );
386 + }
387 +
388 + $end = trim( $parts[1] );
389 + if ( ! is_numeric( $end ) ) {
390 + $end = self::letter_to_number( $end );
391 + }
392 +
393 + // Catch completely out-of-bound ranges, taking into account that the order can be reversed.
394 + if ( ( $start < $min_number && $end < $min_number ) || ( $start > $max_number && $end > $max_number ) ) {
395 + return array();
396 + }
397 +
398 + // Clamp the start and end values to the min and max numbers.
399 + $start = max( $min_number, min( (int) $start, $max_number ) );
400 + $end = max( $min_number, min( (int) $end, $max_number ) );
401 +
402 + return range( $start, $end );
403 + }
404 +
405 + /**
406 + * Converts a comma-separated list of row/column numbers/letters/ranges to a unique and sorted array of numbers.
407 + *
408 + * @since 3.4.0
409 + *
410 + * @param string $item_list A comma-separated list of row/column numbers/letters/ranges.
411 + * @param int $min_number The minimum allowed number for ranges, usually 0 or 1.
412 + * @param int $max_number The maximum allowed number for ranges, usually the number of rows or columns in the table.
413 + * @param array{ unique?: bool, sort?: bool, zero-based?: bool } $post_processing An array of post-processing operations to apply to the resulting array.
414 + * @return int[] The array of numbers in the list, unique and sorted.
415 + */
416 + public static function convert_row_column_list_to_array( string $item_list, int $min_number, int $max_number, array $post_processing = array() ): array {
417 + $original_item_list = explode( ',', $item_list );
418 +
419 + $item_list = array();
420 + foreach ( $original_item_list as $value ) {
421 + if ( str_contains( $value, '-' ) ) {
422 + // Convert ranges.
423 + $range = self::convert_range_to_array( $value, $min_number, $max_number );
424 + $item_list = array_merge( $item_list, $range );
425 + } else {
426 + // Handle single values.
427 + $value = trim( $value );
428 + if ( ! is_numeric( $value ) ) {
429 + $value = self::letter_to_number( $value );
430 + }
431 + if ( $value >= $min_number && $value <= $max_number ) {
432 + $item_list[] = (int) $value;
433 + }
434 + }
435 + }
436 +
437 + // Maybe remove duplicate entries, sort the array, or convert to zero-based numbering.
438 + $post_processing['unique'] ??= true;
439 + if ( $post_processing['unique'] ) {
440 + $item_list = array_unique( $item_list, SORT_NUMERIC );
441 + }
442 +
443 + $post_processing['sort'] ??= true;
444 + if ( $post_processing['sort'] ) {
445 + sort( $item_list, SORT_NUMERIC );
446 + }
447 +
448 + $post_processing['zero-based'] ??= false;
449 + if ( $post_processing['zero-based'] ) {
450 + foreach ( $item_list as &$value ) {
451 + --$value;
452 + }
453 + unset( $value ); // Unset use-by-reference parameter of foreach loop.
454 + }
455 +
456 + return $item_list;
457 + }
458 +
459 + /**
361 460 * Get a nice looking date and time string from the mySQL format of datetime strings for output.
362 461 *
363 462 * @since 1.0.0
364 463 *
@@ -368,13 +467,18 @@
368 467 */
369 468 public static function format_datetime( string $datetime_string, string $separator_or_format = ' ' ): string {
370 469 $timezone = wp_timezone();
371 470 $datetime = date_create( $datetime_string, $timezone );
372 - $timestamp = $datetime->getTimestamp(); // @phpstan-ignore-line
471 + if ( false === $datetime ) {
472 + return $datetime_string;
473 + }
474 + $timestamp = $datetime->getTimestamp();
373 475
374 476 switch ( $separator_or_format ) {
375 477 case ' ':
376 478 case '<br />':
479 + case '<br/>':
480 + case '<br>':
377 481 $date = wp_date( get_option( 'date_format' ), $timestamp, $timezone );
378 482 $time = wp_date( get_option( 'time_format' ), $timestamp, $timezone );
379 483 $output = "{$date}{$separator_or_format}{$time}";
380 484 break;
@@ -395,9 +499,10 @@
395 499 * @return string Nickname of the WP user with the $user_id.
396 500 */
397 501 public static function get_user_display_name( int $user_id ): string {
398 502 $user = get_userdata( $user_id );
399 - return ( isset( $user->display_name ) ) ? $user->display_name : sprintf( '<em>%s</em>', __( 'unknown', 'tablepress' ) );
503 + /* translators: %s: Label for unknown user */
504 + return $user->display_name ?? sprintf( '<em>%s</em>', __( 'unknown', 'tablepress' ) );
400 505 }
401 506
402 507 /**
403 508 * Sanitizes a CSS class to ensure it only contains valid characters.
@@ -418,8 +523,256 @@
418 523 return $sanitized_css_class;
419 524 }
420 525
421 526 /**
527 + * Extracts the top-level keys from a JavaScript object string.
528 + *
529 + * This function is used to extract the keys of the "Custom Commands" JavaScript object string, to check for overrides.
530 + * It covers most cases, like normal object properties with and without quotes, shorthand properties, and shorthand methods,
531 + * and also ignores single-line and multi-line comments.
532 + * It does not cover all possible JavaScript syntax (like template literals, special characters, ...),
533 + * but should be sufficient for the use case.
534 + *
535 + * @since 3.0.0
536 + *
537 + * @param string $js_object_string A JavaScript object as a string.
538 + * @return string[] Array of top-level keys of the object.
539 + */
540 + public static function extract_keys_from_js_object_string( string $js_object_string ): array {
541 + $object_keys = array();
542 + $length = strlen( $js_object_string );
543 + $depth = 0;
544 + $key_expected = true;
545 + $in_quotes = false;
546 + $quote_char = '';
547 + $in_function_declaration = false;
548 + $in_single_line_comment = false;
549 + $in_multi_line_comment = false;
550 + $object_key = '';
551 +
552 + for ( $i = 0; $i < $length; $i++ ) {
553 + $char = $js_object_string[ $i ];
554 +
555 + // Skip parsing single-line comments.
556 + if ( $in_single_line_comment ) {
557 + if ( "\n" === $char ) {
558 + $in_single_line_comment = false;
559 + }
560 + continue;
561 + } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
562 + if ( '/' === $char && $i + 1 < $length && '/' === $js_object_string[ $i + 1 ] ) {
563 + $in_single_line_comment = true;
564 + ++$i; // Skip the second '/'.
565 + continue;
566 + }
567 + }
568 +
569 + // Skip parsing multi-line comments.
570 + if ( $in_multi_line_comment ) {
571 + if ( '*' === $char && $i + 1 < $length && '/' === $js_object_string[ $i + 1 ] ) {
572 + $in_multi_line_comment = false;
573 + ++$i; // Skip the '/' that ends the multi-line comment.
574 + }
575 + continue;
576 + } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
577 + if ( '/' === $char && $i + 1 < $length && '*' === $js_object_string[ $i + 1 ] ) {
578 + $in_multi_line_comment = true;
579 + ++$i; // Skip the '*'.
580 + continue;
581 + }
582 + }
583 +
584 + // Skip parsing while inside a quoted string.
585 + if ( $in_quotes ) {
586 + if ( $quote_char === $char ) {
587 + $in_quotes = false;
588 + }
589 + continue;
590 + } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
591 + if ( '"' === $char || "'" === $char ) {
592 + $in_quotes = true;
593 + $quote_char = $char;
594 + continue;
595 + }
596 + }
597 +
598 + /*
599 + * Skip parsing while inside a `function abc( ... )` declaration string.
600 + * The `$key_expected` check limits search the "function" string to object values.
601 + * The check for the plain `f` reduces expensive `substr()` calls.
602 + */
603 + if ( ! $key_expected ) {
604 + if ( $in_function_declaration ) {
605 + if ( ')' === $char ) {
606 + $in_function_declaration = false;
607 + }
608 + continue;
609 + } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
610 + if ( 'f' === $char && 'function' === substr( $js_object_string, $i, 8 ) ) {
611 + $in_function_declaration = true;
612 + $i += 7; // Skip the rest of the "function" string.
613 + continue;
614 + }
615 + }
616 + }
617 +
618 + // Handle object depth, so that most parsing can be limited to the top level.
619 + if ( '{' === $char || '[' === $char ) {
620 + ++$depth;
621 + }
622 +
623 + // Extract only keys at the top level.
624 + if ( 1 === $depth ) {
625 + if ( $key_expected ) {
626 + if ( ':' === $char ) {
627 + // Check for normal keys, with value after :.
628 +
629 + // Go backwards to find the start of the key.
630 + $j = $i - 1;
631 + while ( $j >= 0 && preg_match( '/\s/', $js_object_string[ $j ] ) ) {
632 + --$j;
633 + }
634 + $key_end = $j; // Position of the last character of the key (potentially with quote).
635 + if ( '"' === $js_object_string[ $j ] || "'" === $js_object_string[ $j ] ) {
636 + // Quoted key.
637 + $quote_char = $js_object_string[ $j ];
638 + --$j;
639 + while ( $j >= 0 && $quote_char !== $js_object_string[ $j ] ) {
640 + --$j;
641 + }
642 + $key_start = $j + 1;
643 + } else {
644 + // Unquoted key.
645 + while ( $j >= 0 && preg_match( '/[\w]/', $js_object_string[ $j ] ) ) {
646 + --$j;
647 + }
648 + $key_start = $j + 1;
649 + }
650 + $object_key = substr( $js_object_string, $key_start, $key_end - $key_start + 1 );
651 + $object_key = trim( $object_key, "\"'" );
652 + if ( '' !== $object_key && ! in_array( $object_key, $object_keys, true ) ) {
653 + $object_keys[] = $object_key;
654 + }
655 + $key_expected = false;
656 + } elseif ( ( ',' === $char || '}' === $char ) ) { // The `}` case is for the last key.
657 + // Check for shorthand properties (which must be unquoted).
658 +
659 + // Go backwards to find the start of the shorthand key.
660 + $j = $i - 1;
661 + while ( $j >= 0 && preg_match( '/\s/', $js_object_string[ $j ] ) ) {
662 + --$j;
663 + }
664 + $key_end = $j; // Position of the last character of the key (without a quote).
665 + while ( $j >= 0 && preg_match( '/[\w]/', $js_object_string[ $j ] ) ) {
666 + --$j;
667 + }
668 + $key_start = $j + 1;
669 + $object_key = substr( $js_object_string, $key_start, $key_end - $key_start + 1 );
670 + if ( '' !== $object_key && ! in_array( $object_key, $object_keys, true ) ) {
671 + $object_keys[] = $object_key;
672 + }
673 + } elseif ( '(' === $char ) {
674 + // Detect shorthand method definitions.
675 +
676 + // Go back to find the start of the method name.
677 + $j = $i - 1;
678 + while ( $j >= 0 && preg_match( '/\s/', $js_object_string[ $j ] ) ) {
679 + --$j;
680 + }
681 + $key_end = $j;
682 + while ( $j >= 0 && preg_match( '/[\w]/', $js_object_string[ $j ] ) ) {
683 + --$j;
684 + }
685 + $key_start = $j + 1;
686 + $object_key = substr( $js_object_string, $key_start, $key_end - $key_start + 1 );
687 + if ( '' !== $object_key && ! in_array( $object_key, $object_keys, true ) ) {
688 + $object_keys[] = $object_key;
689 + }
690 + }
691 + }
692 +
693 + // Reset the "key expected" flag after a comma or closing brace.
694 + if ( ',' === $char || '}' === $char ) {
695 + $key_expected = true;
696 + }
697 + }
698 +
699 + // Handle object depth.
700 + if ( '}' === $char || ']' === $char ) {
701 + --$depth;
702 + }
703 + }
704 +
705 + return $object_keys;
706 + }
707 +
708 + /**
709 + * Converts old DataTables 1.x CSS classes and parameters to the DataTables 2 variants.
710 + *
711 + * This function is used to modernize "Custom CSS" and "Custom Commands" for compatibility with DataTables 2.x.
712 + * It probably does not catch all possible cases.
713 + *
714 + * @since 3.0.0
715 + *
716 + * @param string $code Code that contains DataTables 1.x CSS classes and parameters.
717 + * @return string Updated code with DataTables 2.x CSS classes and parameters.
718 + */
719 + public static function convert_datatables_api_data( string $code ): string {
720 + /**
721 + * Mappings for DataTables 1.x CSS class or parameter to DataTables 2 variants.
722 + * As this array is used in `strtr()`, it's pre-sorted for descending string length of the array keys.
723 + */
724 + static $datatables_api_data_mappings = array(
725 + // CSS classes.
726 + '.tablepress thead .sorting:hover' => '.tablepress thead .dt-orderable-asc:hover,.tablepress thead .dt-orderable-desc:hover',
727 + '.tablepress thead .sorting_desc' => '.tablepress thead .dt-ordering-desc',
728 + '.dataTables_filter label input' => '.dt-container .dt-search input',
729 + '.tablepress thead .sorting_asc' => '.tablepress thead .dt-ordering-asc',
730 + '.dataTables_scrollFootInner' => '.dt-scroll-footInner',
731 + '.dataTables_scrollHeadInner' => '.dt-scroll-headInner',
732 + '.tablepress thead .sorting' => '.tablepress thead .dt-orderable-asc,.tablepress thead .dt-orderable-desc',
733 + '.dataTables_processing' => '.dt-processing',
734 + '.dataTables_scrollBody' => '.dt-scroll-body',
735 + '.dataTables_scrollFoot' => '.dt-scroll-foot',
736 + '.dataTables_scrollHead' => '.dt-scroll-head',
737 + '.dataTables_paginate' => '.dt-paging',
738 + '.tablepress .even td' => '.tablepress>:where(tbody.row-striping)>:nth-child(odd)>*',
739 + '.dataTables_wrapper' => '.dt-container',
740 + '.tablepress .odd td' => '.tablepress>:where(tbody.row-striping)>:nth-child(even)>*',
741 + '.dataTables_filter' => '.dt-search',
742 + '.dataTables_length' => '.dt-length',
743 + '.dataTables_scroll' => '.dt-scroll',
744 + '.dataTables_empty' => '.dt-empty',
745 + '.dataTables_info' => '.dt-info',
746 + '.paginate_button' => '.dt-paging-button',
747 + // DataTables API functions.
748 + '$.fn.dataTable.' => 'DataTable.',
749 + );
750 + $code = strtr( $code, $datatables_api_data_mappings );
751 +
752 + // HTML ID mappings, which were removed.
753 + if ( str_contains( $code, '#tablepress-' ) ) {
754 + $code = (string) preg_replace(
755 + array(
756 + '/#tablepress-([A-Za-z1-9_-]|[A-Za-z0-9_-]{2,})_paginate/',
757 + '/#tablepress-([A-Za-z1-9_-]|[A-Za-z0-9_-]{2,})_filter/',
758 + '/#tablepress-([A-Za-z1-9_-]|[A-Za-z0-9_-]{2,})_length/',
759 + '/#tablepress-([A-Za-z1-9_-]|[A-Za-z0-9_-]{2,})_info/',
760 + ),
761 + array(
762 + '#tablepress-$1_wrapper .dt-paging',
763 + '#tablepress-$1_wrapper .dt-search',
764 + '#tablepress-$1_wrapper .dt-length',
765 + '#tablepress-$1_wrapper .dt-info',
766 + ),
767 + $code,
768 + );
769 + }
770 +
771 + return $code;
772 + }
773 +
774 + /**
422 775 * Retrieves all information of a WP_Error object as a string.
423 776 *
424 777 * @since 1.4.0
425 778 *
@@ -438,10 +791,16 @@
438 791 if ( ! empty( $error_messages ) ) {
439 792 $error_strings[ $error_code ] .= " ({$error_messages})";
440 793 }
441 794 $error_data = $wp_error->get_error_data( $error_code );
442 - if ( ! is_null( $error_data ) ) {
795 + if ( is_string( $error_data ) ) {
443 796 $error_strings[ $error_code ] .= " [{$error_data}]";
797 + } elseif ( is_array( $error_data ) ) {
798 + foreach ( $error_data as $key => $value ) {
799 + $error_data[ $key ] = "{$key}: {$value}";
800 + }
801 + $error_data = implode( ', ', $error_data );
802 + $error_strings[ $error_code ] .= " [{$error_data}]";
444 803 }
445 804 }
446 805 return implode( ";\n", $error_strings );
447 806 }
@@ -488,8 +847,12 @@
488 847 'item' => false,
489 848 );
490 849 $params = array_merge( $default_params, $params );
491 850
851 + if ( isset( $params['error_details'] ) ) {
852 + $params['error_details'] = rawurlencode( $params['error_details'] );
853 + }
854 +
492 855 $url = add_query_arg( $params, admin_url( $target ) );
493 856 if ( $add_nonce ) {
494 857 $url = wp_nonce_url( $url, self::nonce( $nonce_action, $params['item'] ) ); // wp_nonce_url() does esc_html().
495 858 }
@@ -517,22 +880,25 @@
517 880 exit;
518 881 }
519 882
520 883 /**
521 - * Determines whether the site uses the block editor, so that certain text and input fields referring to Shortcodes can be displayed or not.
884 + * Determines the editor that the site uses, so that certain text and input fields referring to Shortcodes can be displayed or not.
522 885 *
523 - * @since 2.0.1
886 + * @since 3.1.0
524 887 *
525 - * @return bool True if the site uses the block editor, false otherwise.
888 + * @return string The editor that the site uses, either "block", "elementor", or "other".
526 889 */
527 - public static function site_uses_block_editor(): bool {
890 + public static function site_used_editor(): string {
891 + if ( is_plugin_active( 'elementor/elementor.php' ) ) {
892 + return 'elementor';
893 + }
894 +
895 + // Checking for Elementor is not needed anymore in this condition.
528 896 $site_uses_block_editor = use_block_editor_for_post_type( 'post' )
529 - && ! is_plugin_active( 'beaver-builder-lite-version/fl-builder.php' )
530 897 && ! is_plugin_active( 'classic-editor/classic-editor.php' )
531 898 && ! is_plugin_active( 'classic-editor-addon/classic-editor-addon.php' )
532 - && ! is_plugin_active( 'elementor/elementor.php' )
533 - && ! is_plugin_active( 'siteorigin-panels/siteorigin-panels.php' );
534 -
899 + && ! is_plugin_active( 'siteorigin-panels/siteorigin-panels.php' )
900 + && ! is_plugin_active( 'beaver-builder-lite-version/fl-builder.php' );
535 901 /**
536 902 * Filters the outcome of the check whether the site uses the block editor.
537 903 *
538 904 * This can be used when certain conditions (e.g. new site builders) are not (yet) accounted for.
@@ -541,238 +907,223 @@
541 907 *
542 908 * @param bool $site_uses_block_editor True if the site uses the block editor, false otherwise.
543 909 */
544 910 $site_uses_block_editor = (bool) apply_filters( 'tablepress_site_uses_block_editor', $site_uses_block_editor );
911 + if ( $site_uses_block_editor ) {
912 + return 'block';
913 + }
545 914
546 - return $site_uses_block_editor;
915 + return 'other';
547 916 }
548 917
549 918 /**
550 - * Initializes the list of TablePress premium modules.
919 + * Adds the (translated) names and descriptions to the list of feature modules.
551 920 *
552 - * @since 2.1.0
921 + * @since 3.3.0
553 922 */
554 - public static function init_modules(): void {
555 - self::$modules = array(
923 + public static function load_modules_data(): void {
924 + // Prevent repeated execution of expensive translation functions via a static variable.
925 + static $modules_initialized = false;
926 + if ( $modules_initialized ) {
927 + return;
928 + }
929 + $modules_initialized = true;
930 +
931 + $modules = array(
556 932 'advanced-access-rights' => array(
557 - 'name' => __( 'Advanced Access Rights', 'tablepress' ),
558 - 'description' => __( 'Restrict access to individual tables for individual users.', 'tablepress' ),
559 - 'category' => 'backend',
560 - 'class' => 'TablePress_Module_Advanced_Access_Rights',
561 - 'incompatible_classes' => array( 'TablePress_Advanced_Access_Rights_Controller' ),
562 - 'minimum_plan' => 'max',
563 - 'default_active' => false,
933 + 'name' => __( 'Advanced Access Rights', 'tablepress' ),
934 + 'description' => __( 'Restrict access to individual tables for individual users.', 'tablepress' ),
564 935 ),
565 936 'automatic-periodic-table-import' => array(
566 - 'name' => __( 'Automatic Periodic Table Import', 'tablepress' ),
567 - 'description' => __( 'Periodically update tables from a configured import source.', 'tablepress' ),
568 - 'category' => 'backend',
569 - 'class' => 'TablePress_Module_Automatic_Periodic_Table_Import',
570 - 'incompatible_classes' => array( 'TablePress_Table_Auto_Update' ),
571 - 'minimum_plan' => 'max',
572 - 'default_active' => true,
937 + 'name' => __( 'Automatic Periodic Table Import', 'tablepress' ),
938 + 'description' => __( 'Periodically update tables from a configured import source.', 'tablepress' ),
573 939 ),
574 940 'automatic-table-export' => array(
575 - 'name' => __( 'Automatic Table Export', 'tablepress' ),
576 - 'description' => __( 'Export and save tables to files on the server after they were modified.', 'tablepress' ),
577 - 'category' => 'backend',
578 - 'class' => 'TablePress_Module_Automatic_Table_Export',
579 - 'incompatible_classes' => array(),
580 - 'minimum_plan' => 'pro',
581 - 'default_active' => false,
941 + 'name' => __( 'Automatic Table Export', 'tablepress' ),
942 + 'description' => __( 'Export and save tables to files on the server after they were modified.', 'tablepress' ),
582 943 ),
583 944 'cell-highlighting' => array(
584 - 'name' => __( 'Cell Highlighting', 'tablepress' ),
585 - 'description' => __( 'Add CSS classes to cells for highlighting based on their content.', 'tablepress' ),
586 - 'category' => 'frontend',
587 - 'class' => 'TablePress_Module_Cell_Highlighting',
588 - 'incompatible_classes' => array( 'TablePress_Cell_Highlighting' ),
589 - 'minimum_plan' => 'pro',
590 - 'default_active' => false,
945 + 'name' => __( 'Cell Highlighting', 'tablepress' ),
946 + 'description' => __( 'Add CSS classes to cells for highlighting based on their content.', 'tablepress' ),
591 947 ),
592 948 'column-order' => array(
593 - 'name' => __( 'Column Order', 'tablepress' ),
594 - 'description' => __( 'Order the columns in different ways when a table is shown.', 'tablepress' ),
595 - 'category' => 'data-management',
596 - 'class' => 'TablePress_Module_Column_Order',
597 - 'incompatible_classes' => array( 'TablePress_Column_Order' ),
598 - 'minimum_plan' => 'pro',
599 - 'default_active' => false,
949 + 'name' => __( 'Column Order', 'tablepress' ),
950 + 'description' => __( 'Order the columns in different ways when a table is shown.', 'tablepress' ),
600 951 ),
601 952 'datatables-advanced-loading' => array(
602 - 'name' => __( 'Advanced Loading', 'tablepress' ),
603 - 'description' => __( 'Load the table data from a JSON array for faster loading.', 'tablepress' ),
604 - 'category' => 'backend',
605 - 'class' => 'TablePress_Module_DataTables_Advanced_Loading',
606 - 'incompatible_classes' => array( 'TablePress_DataTables_Advanced_Loading' ),
607 - 'minimum_plan' => 'max',
608 - 'default_active' => false,
953 + 'name' => __( 'Advanced Loading', 'tablepress' ),
954 + 'description' => __( 'Load the table data from a JSON array for faster loading.', 'tablepress' ),
609 955 ),
610 956 'datatables-alphabetsearch' => array(
611 - 'name' => __( 'Alphabet Search', 'tablepress' ),
612 - 'description' => __( 'Show Alphabet buttons above the table to filter rows by their first letter.', 'tablepress' ),
613 - 'category' => 'search-filter',
614 - 'class' => 'TablePress_Module_DataTables_Alphabetsearch',
615 - 'incompatible_classes' => array(),
616 - 'minimum_plan' => 'pro',
617 - 'default_active' => false,
957 + 'name' => __( 'Alphabet Search', 'tablepress' ),
958 + 'description' => __( 'Show Alphabet buttons above the table to filter rows by their first letter.', 'tablepress' ),
618 959 ),
619 960 'datatables-auto-filter' => array(
620 - 'name' => __( 'Automatic Filter', 'tablepress' ),
621 - 'description' => __( 'Pre-filter a table when it is shown.', 'tablepress' ),
622 - 'category' => 'search-filter',
623 - 'class' => 'TablePress_Module_DataTables_Auto_Filter',
624 - 'incompatible_classes' => array( 'TablePress_DataTables_Auto_Filter' ),
625 - 'minimum_plan' => 'pro',
626 - 'default_active' => false,
961 + 'name' => __( 'Automatic Filter', 'tablepress' ),
962 + 'description' => __( 'Pre-filter a table when it is shown.', 'tablepress' ),
627 963 ),
628 964 'datatables-buttons' => array(
629 - 'name' => __( 'Buttons', 'tablepress' ),
630 - 'description' => __( 'Add buttons for downloading, copying, printing, and changing column visibility of tables.', 'tablepress' ),
631 - 'category' => 'frontend',
632 - 'class' => 'TablePress_Module_DataTables_Buttons',
633 - 'incompatible_classes' => array( 'TablePress_DataTables_Buttons' ),
634 - 'minimum_plan' => 'pro',
635 - 'default_active' => true,
965 + 'name' => __( 'User Action Buttons', 'tablepress' ),
966 + 'description' => __( 'Add buttons for downloading, copying, printing, and changing column visibility of tables.', 'tablepress' ),
636 967 ),
637 968 'datatables-columnfilterwidgets' => array(
638 - 'name' => __( 'Column Filter Dropdowns', 'tablepress' ),
639 - 'description' => __( 'Add a search dropdown for each column above the table.', 'tablepress' ),
640 - 'category' => 'search-filter',
641 - 'class' => 'TablePress_Module_DataTables_ColumnFilterWidgets',
642 - 'incompatible_classes' => array(),
643 - 'minimum_plan' => 'pro',
644 - 'default_active' => true,
969 + 'name' => __( 'Column Filter Dropdowns', 'tablepress' ),
970 + 'description' => __( 'Add a search dropdown for each column above the table.', 'tablepress' ),
645 971 ),
646 972 'datatables-column-filter' => array(
647 - 'name' => __( 'Individual Column Filtering', 'tablepress' ),
648 - 'description' => __( 'Add a search field for each column to the table head or foot row.', 'tablepress' ),
649 - 'category' => 'search-filter',
650 - 'class' => 'TablePress_Module_DataTables_Column_Filter',
651 - 'incompatible_classes' => array(),
652 - 'minimum_plan' => 'pro',
653 - 'default_active' => false,
973 + 'name' => __( 'Individual Column Filtering', 'tablepress' ),
974 + 'description' => __( 'Add a search field or filter dropdown for each column to a table head or foot row.', 'tablepress' ),
654 975 ),
655 976 'datatables-counter-column' => array(
656 - 'name' => __( 'Counter Column', 'tablepress' ),
657 - 'description' => __( 'Make the first column an index or counter column with the row position.', 'tablepress' ),
658 - 'category' => 'frontend',
659 - 'class' => 'TablePress_Module_DataTables_Counter_Column',
660 - 'incompatible_classes' => array(),
661 - 'minimum_plan' => 'pro',
662 - 'default_active' => false,
977 + 'name' => __( 'Index Column', 'tablepress' ),
978 + 'description' => __( 'Make the first column an index or counter column with the row position.', 'tablepress' ),
663 979 ),
664 980 'datatables-fixedheader-fixedcolumns' => array(
665 - 'name' => __( 'Fixed Rows and Columns', 'tablepress' ),
666 - 'description' => __( 'Fix the header and footer row and the first and last column when scrolling the table.', 'tablepress' ),
667 - 'category' => 'frontend',
668 - 'class' => 'TablePress_Module_DataTables_FixedHeader_FixedColumns',
669 - 'incompatible_classes' => array(
670 - 'TablePress_DataTables_FixedHeader',
671 - 'TablePress_DataTables_FixedColumns',
672 - ),
673 - 'minimum_plan' => 'pro',
674 - 'default_active' => true,
981 + 'name' => __( 'Fixed Rows and Columns', 'tablepress' ),
982 + 'description' => __( 'Fix the header and footer row and the first and last column when scrolling the table.', 'tablepress' ),
675 983 ),
984 + 'datatables-layout' => array(
985 + 'name' => __( 'Table Layout', 'tablepress' ),
986 + 'description' => __( 'Customize the layout and position of features around a table.', 'tablepress' ),
987 + ),
988 + 'datatables-fuzzysearch' => array(
989 + 'name' => __( 'Fuzzy Search', 'tablepress' ),
990 + 'description' => __( 'Let the search account for spelling mistakes and typos and find similar matches.', 'tablepress' ),
991 + ),
992 + 'datatables-inverted-filter' => array(
993 + 'name' => __( 'Inverted Filtering', 'tablepress' ),
994 + 'description' => __( 'Turn the filtering into a search and hide the table if no search term is entered.', 'tablepress' ),
995 + ),
996 + 'datatables-pagination' => array(
997 + 'name' => __( 'Advanced Pagination Settings', 'tablepress' ),
998 + 'description' => __( 'Customize the pagination settings of the table.', 'tablepress' ),
999 + ),
676 1000 'datatables-rowgroup' => array(
677 - 'name' => __( 'Row Grouping', 'tablepress' ),
678 - 'description' => __( 'Group table rows by a common keyword, category, or title.', 'tablepress' ),
679 - 'category' => 'frontend',
680 - 'class' => 'TablePress_Module_DataTables_RowGroup',
681 - 'incompatible_classes' => array( 'TablePress_DataTables_RowGroup' ),
682 - 'minimum_plan' => 'pro',
683 - 'default_active' => false,
1001 + 'name' => __( 'Row Grouping', 'tablepress' ),
1002 + 'description' => __( 'Group table rows by a common keyword, category, or title.', 'tablepress' ),
684 1003 ),
685 1004 'datatables-searchbuilder' => array(
686 - 'name' => __( 'Custom Search Builder', 'tablepress' ),
687 - 'description' => __( 'Show a search builder interface for filtering from groups and using conditions.', 'tablepress' ),
688 - 'category' => 'search-filter',
689 - 'class' => 'TablePress_Module_DataTables_SearchBuilder',
690 - 'incompatible_classes' => array(),
691 - 'minimum_plan' => 'max',
692 - 'default_active' => false,
1005 + 'name' => __( 'Custom Search Builder', 'tablepress' ),
1006 + 'description' => __( 'Show a search builder interface for filtering from groups and using conditions.', 'tablepress' ),
693 1007 ),
694 1008 'datatables-searchhighlight' => array(
695 - 'name' => __( 'Search Highlighting', 'tablepress' ),
696 - 'description' => __( 'Highlight found search terms in the table.', 'tablepress' ),
697 - 'category' => 'search-filter',
698 - 'class' => 'TablePress_Module_DataTables_SearchHighlight',
699 - 'incompatible_classes' => array(),
700 - 'minimum_plan' => 'pro',
701 - 'default_active' => false,
1009 + 'name' => __( 'Search Highlighting', 'tablepress' ),
1010 + 'description' => __( 'Highlight found search terms in the table.', 'tablepress' ),
702 1011 ),
703 1012 'datatables-searchpanes' => array(
704 - 'name' => __( 'Search Panes', 'tablepress' ),
705 - 'description' => __( 'Show panes for filtering the columns.', 'tablepress' ),
706 - 'category' => 'search-filter',
707 - 'class' => 'TablePress_Module_DataTables_SearchPanes',
708 - 'incompatible_classes' => array(),
709 - 'minimum_plan' => 'pro',
710 - 'default_active' => false,
1013 + 'name' => __( 'Search Panes', 'tablepress' ),
1014 + 'description' => __( 'Show panes for filtering the columns.', 'tablepress' ),
711 1015 ),
712 1016 'datatables-serverside-processing' => array(
713 - 'name' => __( 'Server-side Processing', 'tablepress' ),
714 - 'description' => __( 'Process sorting, filtering, and pagination on the server for faster loading of large tables.', 'tablepress' ),
715 - 'category' => 'backend',
716 - 'class' => 'TablePress_Module_DataTables_ServerSide_Processing',
717 - 'incompatible_classes' => array(),
718 - 'minimum_plan' => 'max',
719 - 'default_active' => true,
1017 + 'name' => __( 'Server-side Processing', 'tablepress' ),
1018 + 'description' => __( 'Process sorting, filtering, and pagination on the server for faster loading of large tables.', 'tablepress' ),
720 1019 ),
721 1020 'default-style-customizer' => array(
722 - 'name' => __( 'Default Style Customizer', 'tablepress' ),
723 - 'description' => __( 'Change the default styling of your tables in the visual style customizer.', 'tablepress' ),
724 - 'category' => 'frontend',
725 - 'class' => 'TablePress_Module_Default_Style_Customizer',
726 - 'incompatible_classes' => array(),
727 - 'minimum_plan' => 'pro',
728 - 'default_active' => true,
1021 + 'name' => __( 'Default Style Customizer', 'tablepress' ),
1022 + 'description' => __( 'Change the default styling of your tables in the visual style customizer.', 'tablepress' ),
729 1023 ),
1024 + 'email-notifications' => array(
1025 + 'name' => __( 'Email Notifications', 'tablepress' ),
1026 + 'description' => __( 'Get email notifications when certain actions are performed on tables.', 'tablepress' ),
1027 + ),
730 1028 'responsive-tables' => array(
731 - 'name' => __( 'Responsive Tables', 'tablepress' ),
732 - 'description' => __( 'Make your tables look good on different screen sizes.', 'tablepress' ),
733 - 'category' => 'frontend',
734 - 'class' => 'TablePress_Module_Responsive_Tables',
735 - 'incompatible_classes' => array( 'TablePress_Responsive_Tables' ),
736 - 'minimum_plan' => 'pro',
737 - 'default_active' => true,
1029 + 'name' => __( 'Responsive Tables', 'tablepress' ),
1030 + 'description' => __( 'Make your tables look good on different screen sizes.', 'tablepress' ),
738 1031 ),
739 1032 'rest-api' => array(
740 - 'name' => __( 'REST API', 'tablepress' ),
741 - 'description' => __( 'Read table data via the WordPress REST API, e.g. in external apps.', 'tablepress' ),
742 - 'category' => 'backend',
743 - 'class' => 'TablePress_Module_REST_API',
744 - 'incompatible_classes' => array( 'TablePress_REST_API_Controller' ),
745 - 'minimum_plan' => 'max',
746 - 'default_active' => false,
1033 + 'name' => __( 'REST API', 'tablepress' ),
1034 + 'description' => __( 'Read table data via the WordPress REST API, e.g. in external apps.', 'tablepress' ),
747 1035 ),
748 1036 'row-filtering' => array(
749 - 'name' => __( 'Row Filtering', 'tablepress' ),
750 - 'description' => __( 'Show only table rows that contain defined keywords.', 'tablepress' ),
751 - 'category' => 'data-management',
752 - 'class' => 'TablePress_Module_Row_Filtering',
753 - 'incompatible_classes' => array( 'TablePress_Row_Filter' ),
754 - 'minimum_plan' => 'pro',
755 - 'default_active' => true,
1037 + 'name' => __( 'Row Filtering', 'tablepress' ),
1038 + 'description' => __( 'Show only table rows that contain defined keywords.', 'tablepress' ),
756 1039 ),
757 1040 'row-highlighting' => array(
758 - 'name' => __( 'Row Highlighting', 'tablepress' ),
759 - 'description' => __( 'Add CSS classes to rows for highlighting based on their content.', 'tablepress' ),
760 - 'category' => 'frontend',
761 - 'class' => 'TablePress_Module_Row_Highlighting',
762 - 'incompatible_classes' => array( 'TablePress_Row_Highlighting' ),
763 - 'minimum_plan' => 'pro',
764 - 'default_active' => false,
1041 + 'name' => __( 'Row Highlighting', 'tablepress' ),
1042 + 'description' => __( 'Add CSS classes to rows for highlighting based on their content.', 'tablepress' ),
765 1043 ),
766 1044 'row-order' => array(
767 - 'name' => __( 'Row Order', 'tablepress' ),
768 - 'description' => __( 'Order the rows in different ways when a table is shown.', 'tablepress' ),
769 - 'category' => 'data-management',
770 - 'class' => 'TablePress_Module_Row_Order',
771 - 'incompatible_classes' => array( 'TablePress_Row_Order' ),
772 - 'minimum_plan' => 'pro',
773 - 'default_active' => false,
1045 + 'name' => __( 'Row Order', 'tablepress' ),
1046 + 'description' => __( 'Order the rows in different ways when a table is shown.', 'tablepress' ),
774 1047 ),
775 1048 );
1049 +
1050 + // Append translated module names and descriptions to potentially existing module meta data.
1051 + self::$modules = array_merge_recursive( self::$modules, $modules );
1052 + }
1053 +
1054 + /**
1055 + * Enqueues a CSS file, possibly with dependencies.
1056 + *
1057 + * @since 3.3.0
1058 + *
1059 + * @param string $name Name of the CSS file, without extension.
1060 + * @param string[] $dependencies Optional. List of names of CSS stylesheets that this stylesheet depends on, and which need to be included before this one.
1061 + * @param string $path Optional. Path to the CSS file.
1062 + */
1063 + public static function enqueue_style( string $name, array $dependencies = array(), string $path = 'admin/css/build/' ): void {
1064 + $css_file = "{$path}{$name}.css";
1065 + $css_url = plugins_url( $css_file, TABLEPRESS__FILE__ );
1066 + wp_enqueue_style( "tablepress-{$name}", $css_url, $dependencies, self::version );
1067 + }
1068 +
1069 + /**
1070 + * Enqueues a JavaScript file, possibly with dependencies and extra information.
1071 + *
1072 + * @since 3.3.0
1073 + *
1074 + * @param string $name Name of the JS file, without extension.
1075 + * @param string[] $dependencies Optional. List of names of JS scripts that this script depends on, and which need to be included before this one.
1076 + * @param array<string, mixed> $script_data Optional. JS data that is printed to the page before the script is included. The array key will be used as the name, the value will be JSON encoded.
1077 + * @param string $path Optional. Path to the JS file.
1078 + *
1079 + * @phpstan-param non-empty-string $name
1080 + * @phpstan-param non-empty-string[] $dependencies
1081 + */
1082 + public static function enqueue_script( string $name, array $dependencies = array(), array $script_data = array(), string $path = 'admin/js/build/' ): void {
1083 + $js_file = "{$path}{$name}.js";
1084 + $js_url = plugins_url( $js_file, TABLEPRESS__FILE__ );
1085 +
1086 + $version = self::version;
1087 +
1088 + // Load dependencies and version from the auto-generated asset PHP file.
1089 + $script_asset_path = TABLEPRESS_ABSPATH . "{$path}{$name}.asset.php";
1090 + if ( file_exists( $script_asset_path ) ) {
1091 + $script_asset = require $script_asset_path;
1092 + if ( isset( $script_asset['dependencies'] ) ) {
1093 + $dependencies = array_merge( $dependencies, $script_asset['dependencies'] );
1094 + }
1095 + if ( isset( $script_asset['version'] ) ) {
1096 + $version = $script_asset['version'];
1097 + }
1098 + }
1099 +
1100 + /**
1101 + * Filters the dependencies of a TablePress script file.
1102 + *
1103 + * @since 2.0.0
1104 + *
1105 + * @param string[] $dependencies List of the dependencies that the $name script relies on.
1106 + * @param string $name Name of the JS script, without extension.
1107 + *
1108 + * @phpstan-param non-empty-string[] $dependencies
1109 + */
1110 + $dependencies = apply_filters( 'tablepress_admin_page_script_dependencies', $dependencies, $name );
1111 +
1112 + $script_name = "tablepress-{$name}";
1113 +
1114 + wp_enqueue_script( $script_name, $js_url, $dependencies, $version, array( 'in_footer' => true ) );
1115 +
1116 + // Load JavaScript translation files, for all scripts that rely on `wp-i18n`.
1117 + if ( in_array( 'wp-i18n', $dependencies, true ) ) {
1118 + wp_set_script_translations( $script_name, 'tablepress' );
1119 + }
1120 +
1121 + if ( ! empty( $script_data ) ) {
1122 + foreach ( $script_data as $var_name => $var_data ) {
1123 + $var_data = wp_json_encode( $var_data, JSON_FORCE_OBJECT | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES );
1124 + wp_add_inline_script( $script_name, "const tablepress_{$var_name} = {$var_data};", 'before' );
1125 + }
1126 + }
776 1127 }
777 1128
778 1129 } // class TablePress