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 +596 -260 2.1.8 → 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 - const version = '2.1.8'; // 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 - const db_version = 64; // 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 *
@@ -47,33 +49,30 @@
47 49 *
48 50 * @since 1.0.0
49 51 * @const int
50 52 */
51 - const table_scheme_version = 3; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
53 + public const table_scheme_version = 3; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
52 54
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
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 *
@@ -106,9 +105,9 @@
106 105 * Start-up TablePress (run on WordPress "init") and load the controller for the current state.
107 106 *
108 107 * @since 1.0.0
109 108 */
110 - public static function run() {
109 + public static function run(): void {
111 110 /**
112 111 * Fires before TablePress is loaded.
113 112 *
114 113 * The `tablepress_loaded` action hook might be a better choice in most situations, as TablePress options will then be available.
@@ -116,25 +115,14 @@
116 115 * @since 1.0.0
117 116 */
118 117 do_action( 'tablepress_run' );
119 118
120 - // Check if minimum requirements are fulfilled, currently WordPress 5.8.
121 - include ABSPATH . WPINC . '/version.php'; // Include an unmodified $wp_version.
122 - if ( version_compare( str_replace( '-src', '', $wp_version ), '5.8', '<' ) ) {
123 - // Show error notice to admins, if WP is not installed in the minimum required version, in which case TablePress will not work.
124 - if ( current_user_can( 'update_plugins' ) ) {
125 - add_action( 'admin_notices', array( 'TablePress', 'show_minimum_requirements_error_notice' ) );
126 - }
127 - // And exit TablePress.
128 - return;
129 - }
130 -
131 119 /**
132 120 * Filters the string that is used as the [table] Shortcode.
133 121 *
134 122 * @since 1.0.0
135 123 *
136 - * @param string $shortcode The [table] Shortcode string.
124 + * @param non-empty-string $shortcode The [table] Shortcode string.
137 125 */
138 126 self::$shortcode = apply_filters( 'tablepress_table_shortcode', self::$shortcode );
139 127 /**
140 128 * Filters the string that is used as the [table-info] Shortcode.
@@ -140,9 +128,9 @@
140 128 * Filters the string that is used as the [table-info] Shortcode.
141 129 *
142 130 * @since 1.0.0
143 131 *
144 - * @param string $shortcode_info The [table-info] Shortcode string.
132 + * @param non-empty-string $shortcode_info The [table-info] Shortcode string.
145 133 */
146 134 self::$shortcode_info = apply_filters( 'tablepress_table_info_shortcode', self::$shortcode_info );
147 135
148 136 // Load modals for table and options, to be accessible from everywhere via `TablePress::$model_options` and `TablePress::$model_table`.
@@ -169,9 +157,9 @@
169 157
170 158 if ( is_admin() ) {
171 159 $controller = 'admin';
172 160 if ( wp_doing_ajax() ) {
173 - $controller .= '_ajax';
161 + $controller = 'admin_ajax';
174 162 }
175 163 self::load_controller( $controller );
176 164 }
177 165 // Load the frontend controller in all scenarios, so that Shortcode render functions are always available.
@@ -176,8 +164,13 @@
176 164 }
177 165 // Load the frontend controller in all scenarios, so that Shortcode render functions are always available.
178 166 self::$controller = self::load_controller( 'frontend' );
179 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 +
180 173 /**
181 174 * Fires after TablePress is loaded.
182 175 *
183 176 * The `tablepress_run` action hook can be used if code has to run before TablePress is loaded.
@@ -194,9 +187,9 @@
194 187 *
195 188 * @param string $file Name of the PHP file.
196 189 * @param string $folder Name of the folder with the file.
197 190 */
198 - public static function load_file( $file, $folder ) {
191 + public static function load_file( string $file, string $folder ): void {
199 192 $full_path = TABLEPRESS_ABSPATH . $folder . '/' . $file;
200 193 /**
201 194 * Filters the full path of a file that shall be loaded.
202 195 *
@@ -217,15 +210,15 @@
217 210 * of the plugin's directory.
218 211 *
219 212 * @since 1.0.0
220 213 *
221 - * @param string $class_name Name of the class.
222 - * @param string $file Name of the PHP file with the class.
223 - * @param string $folder Name of the folder with $class_name's $file.
224 - * @param mixed $params Optional. Parameters that are passed to the constructor of $class_name.
214 + * @param string $class_name Name of the class.
215 + * @param string $file Name of the PHP file with the class.
216 + * @param string $folder Name of the folder with $class_name's $file.
217 + * @param mixed[]|string|null $params Optional. Parameters that are passed to the constructor of $class_name.
225 218 * @return object Initialized instance of the class.
226 219 */
227 - public static function load_class( $class_name, $file, $folder, $params = null ) {
220 + public static function load_class( string $class_name, string $file, string $folder, /* ?array|string */ $params = null ): object {
228 221 /**
229 222 * Filters name of the class that shall be loaded.
230 223 *
231 224 * @since 1.0.0
@@ -247,9 +240,9 @@
247 240 *
248 241 * @param string $model Name of the model.
249 242 * @return object Instance of the initialized model.
250 243 */
251 - public static function load_model( $model ) {
244 + public static function load_model( string $model ): object {
252 245 // Model Base Class.
253 246 self::load_file( 'class-model.php', 'classes' );
254 247 // Make first letter uppercase for a better looking naming pattern.
255 248 $ucmodel = ucfirst( $model );
@@ -261,13 +254,13 @@
261 254 * Create a new instance of the $view, which is stored in the "views" subfolder, and set it up with $data.
262 255 *
263 256 * @since 1.0.0
264 257 *
265 - * @param string $view Name of the view to load.
266 - * @param array $data Optional. Parameters/PHP variables that shall be available to the view.
258 + * @param string $view Name of the view to load.
259 + * @param array<string, mixed> $data Optional. Parameters/PHP variables that shall be available to the view.
267 260 * @return object Instance of the initialized view, already set up, just needs to be rendered.
268 261 */
269 - public static function load_view( $view, array $data = array() ) {
262 + public static function load_view( string $view, array $data = array() ): object {
270 263 // View Base Class.
271 264 self::load_file( 'class-view.php', 'classes' );
272 265 // Make first letter uppercase for a better looking naming pattern.
273 266 $ucview = ucfirst( $view );
@@ -283,9 +276,9 @@
283 276 *
284 277 * @param string $controller Name of the controller.
285 278 * @return object Instance of the initialized controller.
286 279 */
287 - public static function load_controller( $controller ) {
280 + public static function load_controller( string $controller ): object {
288 281 // Controller Base Class.
289 282 self::load_file( 'class-controller.php', 'classes' );
290 283 // Make first letter uppercase for a better looking naming pattern.
291 284 $uccontroller = ucfirst( $controller );
@@ -297,13 +290,13 @@
297 290 * Generate the complete nonce string, from the nonce base, the action and an item, e.g. tablepress_delete_table_3.
298 291 *
299 292 * @since 1.0.0
300 293 *
301 - * @param string $action Action for which the nonce is needed.
302 - * @param string|bool $item Optional. Item for which the action will be performed, like "table".
294 + * @param string $action Action for which the nonce is needed.
295 + * @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.
303 296 * @return string The resulting nonce string.
304 297 */
305 - public static function nonce( $action, $item = false ) {
298 + public static function nonce( string $action, /* string|false */ $item = false ): string {
306 299 $nonce = "tablepress_{$action}";
307 300 if ( $item ) {
308 301 $nonce .= "_{$item}";
309 302 }
@@ -314,14 +307,14 @@
314 307 * Check whether a nonce string is valid.
315 308 *
316 309 * @since 1.0.0
317 310 *
318 - * @param string $action Action for which the nonce should be checked.
319 - * @param string|bool $item Optional. Item for which the action should be performed, like "table".
320 - * @param string $query_arg Optional. Name of the nonce query string argument in $_POST.
321 - * @param bool $ajax Whether the nonce comes from an AJAX request.
311 + * @param string $action Action for which the nonce should be checked.
312 + * @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.
313 + * @param string $query_arg Optional. Name of the nonce query string argument in $_POST.
314 + * @param bool $ajax Whether the nonce comes from an AJAX request.
322 315 */
323 - public static function check_nonce( $action, $item = false, $query_arg = '_wpnonce', $ajax = false ) {
316 + public static function check_nonce( string $action, /* string|false */ $item = false, string $query_arg = '_wpnonce', bool $ajax = false ): void {
324 317 $nonce_action = self::nonce( $action, $item );
325 318 if ( $ajax ) {
326 319 check_ajax_referer( $nonce_action, $query_arg );
327 320 } else {
@@ -338,14 +331,15 @@
338 331 *
339 332 * @param string $column Column string.
340 333 * @return int Column number, 1-based.
341 334 */
342 - public static function letter_to_number( $column ) {
335 + public static function letter_to_number( string $column ): int {
336 + $column = (string) preg_replace( '/[^A-Za-z]/', '', $column );
343 337 $column = strtoupper( $column );
344 338 $count = strlen( $column );
345 339 $number = 0;
346 340 for ( $i = 0; $i < $count; $i++ ) {
347 - $number += ( ord( $column[ $count - 1 - $i ] ) - 64 ) * pow( 26, $i );
341 + $number += ( ord( $column[ $count - 1 - $i ] ) - 64 ) * 26 ** $i;
348 342 }
349 343 return $number;
350 344 }
351 345
@@ -358,18 +352,112 @@
358 352 *
359 353 * @param int $number Column number, 1-based.
360 354 * @return string Column string.
361 355 */
362 - public static function number_to_letter( $number ) {
356 + public static function number_to_letter( int $number ): string {
363 357 $column = '';
364 358 while ( $number > 0 ) {
365 359 $column = chr( 65 + ( ( $number - 1 ) % 26 ) ) . $column;
366 - $number = floor( ( $number - 1 ) / 26 );
360 + $number = intdiv( $number - 1, 26 );
367 361 }
368 362 return $column;
369 363 }
370 364
371 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 + /**
372 460 * Get a nice looking date and time string from the mySQL format of datetime strings for output.
373 461 *
374 462 * @since 1.0.0
375 463 *
@@ -376,22 +464,27 @@
376 464 * @param string $datetime_string DateTime string, often in mySQL format..
377 465 * @param string $separator_or_format Optional. Separator between date and time, or format string.
378 466 * @return string Nice looking string with the date and time.
379 467 */
380 - public static function format_datetime( $datetime_string, $separator_or_format = ' ' ) {
468 + public static function format_datetime( string $datetime_string, string $separator_or_format = ' ' ): string {
381 469 $timezone = wp_timezone();
382 470 $datetime = date_create( $datetime_string, $timezone );
471 + if ( false === $datetime ) {
472 + return $datetime_string;
473 + }
383 474 $timestamp = $datetime->getTimestamp();
384 475
385 476 switch ( $separator_or_format ) {
386 477 case ' ':
387 478 case '<br />':
479 + case '<br/>':
480 + case '<br>':
388 481 $date = wp_date( get_option( 'date_format' ), $timestamp, $timezone );
389 482 $time = wp_date( get_option( 'time_format' ), $timestamp, $timezone );
390 483 $output = "{$date}{$separator_or_format}{$time}";
391 484 break;
392 485 default:
393 - $output = wp_date( $separator_or_format, $timestamp, $timezone );
486 + $output = (string) wp_date( $separator_or_format, $timestamp, $timezone );
394 487 break;
395 488 }
396 489
397 490 return $output;
@@ -404,11 +497,12 @@
404 497 *
405 498 * @param int $user_id WP user ID.
406 499 * @return string Nickname of the WP user with the $user_id.
407 500 */
408 - public static function get_user_display_name( $user_id ) {
501 + public static function get_user_display_name( int $user_id ): string {
409 502 $user = get_userdata( $user_id );
410 - 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' ) );
411 505 }
412 506
413 507 /**
414 508 * Sanitizes a CSS class to ensure it only contains valid characters.
@@ -420,17 +514,265 @@
420 514 *
421 515 * @param string $css_class The CSS class name to be sanitized.
422 516 * @return string The sanitized CSS class.
423 517 */
424 - public static function sanitize_css_class( $css_class ) {
518 + public static function sanitize_css_class( string $css_class ): string {
425 519 // Strip out any %-encoded octets.
426 - $sanitized_css_class = preg_replace( '|%[a-fA-F0-9][a-fA-F0-9]|', '', $css_class );
520 + $sanitized_css_class = (string) preg_replace( '|%[a-fA-F0-9][a-fA-F0-9]|', '', $css_class );
427 521 // Limit to A-Z, a-z, 0-9, ':', '_', and '-'.
428 - $sanitized_css_class = preg_replace( '/[^A-Za-z0-9:_-]/', '', $sanitized_css_class );
522 + $sanitized_css_class = (string) preg_replace( '/[^A-Za-z0-9:_-]/', '', $sanitized_css_class );
429 523 return $sanitized_css_class;
430 524 }
431 525
432 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 + /**
433 775 * Retrieves all information of a WP_Error object as a string.
434 776 *
435 777 * @since 1.4.0
436 778 *
@@ -436,9 +778,9 @@
436 778 *
437 779 * @param WP_Error $wp_error A WP_Error object.
438 780 * @return string All error codes, messages, and data of the WP_Error.
439 781 */
440 - public static function get_wp_error_string( $wp_error ) {
782 + public static function get_wp_error_string( WP_Error $wp_error ): string {
441 783 $error_strings = array();
442 784 $error_codes = $wp_error->get_error_codes();
443 785 // Reverse order to get latest errors first.
444 786 $error_codes = array_reverse( $error_codes );
@@ -449,10 +791,16 @@
449 791 if ( ! empty( $error_messages ) ) {
450 792 $error_strings[ $error_code ] .= " ({$error_messages})";
451 793 }
452 794 $error_data = $wp_error->get_error_data( $error_code );
453 - if ( ! is_null( $error_data ) ) {
795 + if ( is_string( $error_data ) ) {
454 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}]";
455 803 }
456 804 }
457 805 return implode( ";\n", $error_strings );
458 806 }
@@ -461,14 +809,14 @@
461 809 * Generate the action URL, to be used as a link within the plugin (e.g. in the submenu navigation or List of Tables).
462 810 *
463 811 * @since 1.0.0
464 812 *
465 - * @param array $params Optional. Parameters to form the query string of the URL.
466 - * @param bool $add_nonce Optional. Whether the URL shall be nonced by WordPress.
467 - * @param string $target Optional. Target File, e.g. "admin-post.php" for POST requests.
813 + * @param array<string, mixed> $params Optional. Parameters to form the query string of the URL.
814 + * @param bool $add_nonce Optional. Whether the URL shall be nonced by WordPress.
815 + * @param string $target Optional. Target File, e.g. "admin-post.php" for POST requests.
468 816 * @return string The URL for the given parameters (already run through esc_url() with $add_nonce === true!).
469 817 */
470 - public static function url( array $params = array(), $add_nonce = false, $target = '' ) {
818 + public static function url( array $params = array(), bool $add_nonce = false, string $target = '' ): string {
471 819 // Default action is "list", if no action given.
472 820 if ( ! isset( $params['action'] ) ) {
473 821 $params['action'] = 'list';
474 822 }
@@ -499,8 +847,12 @@
499 847 'item' => false,
500 848 );
501 849 $params = array_merge( $default_params, $params );
502 850
851 + if ( isset( $params['error_details'] ) ) {
852 + $params['error_details'] = rawurlencode( $params['error_details'] );
853 + }
854 +
503 855 $url = add_query_arg( $params, admin_url( $target ) );
504 856 if ( $add_nonce ) {
505 857 $url = wp_nonce_url( $url, self::nonce( $nonce_action, $params['item'] ) ); // wp_nonce_url() does esc_html().
506 858 }
@@ -511,12 +863,12 @@
511 863 * Create a redirect URL from the $target_parameters and redirect the user.
512 864 *
513 865 * @since 1.0.0
514 866 *
515 - * @param array $params Optional. Parameters from which the target URL is constructed.
516 - * @param bool $add_nonce Optional. Whether the URL shall be nonced by WordPress.
867 + * @param array<string, mixed> $params Optional. Parameters from which the target URL is constructed.
868 + * @param bool $add_nonce Optional. Whether the URL shall be nonced by WordPress.
517 869 */
518 - public static function redirect( array $params = array(), $add_nonce = false ) {
870 + public static function redirect( array $params = array(), bool $add_nonce = false ): void {
519 871 $redirect = self::url( $params );
520 872 if ( $add_nonce ) {
521 873 if ( ! isset( $params['item'] ) ) {
522 874 $params['item'] = false;
@@ -528,35 +880,25 @@
528 880 exit;
529 881 }
530 882
531 883 /**
532 - * Show an error notice to admins, if TablePress's minimum requirements are not reached.
884 + * Determines the editor that the site uses, so that certain text and input fields referring to Shortcodes can be displayed or not.
533 885 *
534 - * @since 1.0.0
886 + * @since 3.1.0
887 + *
888 + * @return string The editor that the site uses, either "block", "elementor", or "other".
535 889 */
536 - public static function show_minimum_requirements_error_notice() {
537 - // Message is not translated as it is shown on every admin screen, for which we don't want to load translations.
538 - echo '<div class="notice notice-error form-invalid"><p>' .
539 - '<strong>Attention:</strong> ' .
540 - 'The installed version of WordPress is too old for the TablePress plugin! TablePress requires an up-to-date version! <strong>Please <a href="' . esc_url( admin_url( 'update-core.php' ) ) . '">update your WordPress installation</a></strong>!' .
541 - "</p></div>\n";
542 - }
890 + public static function site_used_editor(): string {
891 + if ( is_plugin_active( 'elementor/elementor.php' ) ) {
892 + return 'elementor';
893 + }
543 894
544 - /**
545 - * Determines whether the site uses the block editor, so that certain text and input fields referring to Shortcodes can be displayed or not.
546 - *
547 - * @since 2.0.1
548 - *
549 - * @return bool True if the site uses the block editor, false otherwise.
550 - */
551 - public static function site_uses_block_editor() {
895 + // Checking for Elementor is not needed anymore in this condition.
552 896 $site_uses_block_editor = use_block_editor_for_post_type( 'post' )
553 - && ! is_plugin_active( 'beaver-builder-lite-version/fl-builder.php' )
554 897 && ! is_plugin_active( 'classic-editor/classic-editor.php' )
555 898 && ! is_plugin_active( 'classic-editor-addon/classic-editor-addon.php' )
556 - && ! is_plugin_active( 'elementor/elementor.php' )
557 - && ! is_plugin_active( 'siteorigin-panels/siteorigin-panels.php' );
558 -
899 + && ! is_plugin_active( 'siteorigin-panels/siteorigin-panels.php' )
900 + && ! is_plugin_active( 'beaver-builder-lite-version/fl-builder.php' );
559 901 /**
560 902 * Filters the outcome of the check whether the site uses the block editor.
561 903 *
562 904 * This can be used when certain conditions (e.g. new site builders) are not (yet) accounted for.
@@ -564,230 +906,224 @@
564 906 * @since 2.0.1
565 907 *
566 908 * @param bool $site_uses_block_editor True if the site uses the block editor, false otherwise.
567 909 */
568 - $site_uses_block_editor = apply_filters( 'tablepress_site_uses_block_editor', $site_uses_block_editor );
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 + }
569 914
570 - return $site_uses_block_editor;
915 + return 'other';
571 916 }
572 917
573 918 /**
574 - * Initializes the list of TablePress premium modules.
919 + * Adds the (translated) names and descriptions to the list of feature modules.
575 920 *
576 - * @since 2.1.0
921 + * @since 3.3.0
577 922 */
578 - public static function init_modules() {
579 - 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(
580 932 'advanced-access-rights' => array(
581 - 'name' => __( 'Advanced Access Rights', 'tablepress' ),
582 - 'description' => __( 'Restrict access to individual tables for individual users.', 'tablepress' ),
583 - 'category' => 'backend',
584 - 'class' => 'TablePress_Module_Advanced_Access_Rights',
585 - 'incompatible_classes' => array( 'TablePress_Advanced_Access_Rights_Controller' ),
586 - 'minimum_plan' => 'max',
587 - 'default_active' => false,
933 + 'name' => __( 'Advanced Access Rights', 'tablepress' ),
934 + 'description' => __( 'Restrict access to individual tables for individual users.', 'tablepress' ),
588 935 ),
589 936 'automatic-periodic-table-import' => array(
590 - 'name' => __( 'Automatic Periodic Table Import', 'tablepress' ),
591 - 'description' => __( 'Periodically update tables from a configured import source.', 'tablepress' ),
592 - 'category' => 'backend',
593 - 'class' => 'TablePress_Module_Automatic_Periodic_Table_Import',
594 - 'incompatible_classes' => array( 'TablePress_Table_Auto_Update' ),
595 - 'minimum_plan' => 'max',
596 - 'default_active' => true,
937 + 'name' => __( 'Automatic Periodic Table Import', 'tablepress' ),
938 + 'description' => __( 'Periodically update tables from a configured import source.', 'tablepress' ),
597 939 ),
598 940 'automatic-table-export' => array(
599 - 'name' => __( 'Automatic Table Export', 'tablepress' ),
600 - 'description' => __( 'Export and save tables to files on the server after they were modified.', 'tablepress' ),
601 - 'category' => 'backend',
602 - 'class' => 'TablePress_Module_Automatic_Table_Export',
603 - 'incompatible_classes' => array(),
604 - 'minimum_plan' => 'pro',
605 - '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' ),
606 943 ),
607 944 'cell-highlighting' => array(
608 - 'name' => __( 'Cell Highlighting', 'tablepress' ),
609 - 'description' => __( 'Add CSS classes to cells for highlighting based on their content.', 'tablepress' ),
610 - 'category' => 'frontend',
611 - 'class' => 'TablePress_Module_Cell_Highlighting',
612 - 'incompatible_classes' => array( 'TablePress_Cell_Highlighting' ),
613 - 'minimum_plan' => 'pro',
614 - 'default_active' => false,
945 + 'name' => __( 'Cell Highlighting', 'tablepress' ),
946 + 'description' => __( 'Add CSS classes to cells for highlighting based on their content.', 'tablepress' ),
615 947 ),
616 948 'column-order' => array(
617 - 'name' => __( 'Column Order', 'tablepress' ),
618 - 'description' => __( 'Order the columns in different ways when a table is shown.', 'tablepress' ),
619 - 'category' => 'data-management',
620 - 'class' => 'TablePress_Module_Column_Order',
621 - 'incompatible_classes' => array( 'TablePress_Column_Order' ),
622 - 'minimum_plan' => 'pro',
623 - 'default_active' => false,
949 + 'name' => __( 'Column Order', 'tablepress' ),
950 + 'description' => __( 'Order the columns in different ways when a table is shown.', 'tablepress' ),
624 951 ),
625 952 'datatables-advanced-loading' => array(
626 - 'name' => __( 'Advanced Loading', 'tablepress' ),
627 - 'description' => __( 'Load the table data from a JSON array for faster loading.', 'tablepress' ),
628 - 'category' => 'backend',
629 - 'class' => 'TablePress_Module_DataTables_Advanced_Loading',
630 - 'incompatible_classes' => array( 'TablePress_DataTables_Advanced_Loading' ),
631 - 'minimum_plan' => 'max',
632 - 'default_active' => false,
953 + 'name' => __( 'Advanced Loading', 'tablepress' ),
954 + 'description' => __( 'Load the table data from a JSON array for faster loading.', 'tablepress' ),
633 955 ),
634 956 'datatables-alphabetsearch' => array(
635 - 'name' => __( 'Alphabet Search', 'tablepress' ),
636 - 'description' => __( 'Show Alphabet buttons above the table to filter rows by their first letter.', 'tablepress' ),
637 - 'category' => 'search-filter',
638 - 'class' => 'TablePress_Module_DataTables_Alphabetsearch',
639 - 'incompatible_classes' => array(),
640 - 'minimum_plan' => 'pro',
641 - 'default_active' => false,
957 + 'name' => __( 'Alphabet Search', 'tablepress' ),
958 + 'description' => __( 'Show Alphabet buttons above the table to filter rows by their first letter.', 'tablepress' ),
642 959 ),
643 960 'datatables-auto-filter' => array(
644 - 'name' => __( 'Automatic Filter', 'tablepress' ),
645 - 'description' => __( 'Pre-filter a table when it is shown.', 'tablepress' ),
646 - 'category' => 'search-filter',
647 - 'class' => 'TablePress_Module_DataTables_Auto_Filter',
648 - 'incompatible_classes' => array( 'TablePress_DataTables_Auto_Filter' ),
649 - 'minimum_plan' => 'pro',
650 - 'default_active' => false,
961 + 'name' => __( 'Automatic Filter', 'tablepress' ),
962 + 'description' => __( 'Pre-filter a table when it is shown.', 'tablepress' ),
651 963 ),
652 964 'datatables-buttons' => array(
653 - 'name' => __( 'Buttons', 'tablepress' ),
654 - 'description' => __( 'Add buttons for downloading, copying, printing, and changing column visibility of tables.', 'tablepress' ),
655 - 'category' => 'frontend',
656 - 'class' => 'TablePress_Module_DataTables_Buttons',
657 - 'incompatible_classes' => array( 'TablePress_DataTables_Buttons' ),
658 - 'minimum_plan' => 'pro',
659 - 'default_active' => true,
965 + 'name' => __( 'User Action Buttons', 'tablepress' ),
966 + 'description' => __( 'Add buttons for downloading, copying, printing, and changing column visibility of tables.', 'tablepress' ),
660 967 ),
661 968 'datatables-columnfilterwidgets' => array(
662 - 'name' => __( 'Column Filter Dropdowns', 'tablepress' ),
663 - 'description' => __( 'Add a search dropdown for each column above the table.', 'tablepress' ),
664 - 'category' => 'search-filter',
665 - 'class' => 'TablePress_Module_DataTables_ColumnFilterWidgets',
666 - 'incompatible_classes' => array(),
667 - 'minimum_plan' => 'pro',
668 - 'default_active' => true,
969 + 'name' => __( 'Column Filter Dropdowns', 'tablepress' ),
970 + 'description' => __( 'Add a search dropdown for each column above the table.', 'tablepress' ),
669 971 ),
670 972 'datatables-column-filter' => array(
671 - 'name' => __( 'Individual Column Filtering', 'tablepress' ),
672 - 'description' => __( 'Add a search field for each column to the table head or foot row.', 'tablepress' ),
673 - 'category' => 'search-filter',
674 - 'class' => 'TablePress_Module_DataTables_Column_Filter',
675 - 'incompatible_classes' => array(),
676 - 'minimum_plan' => 'pro',
677 - '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' ),
678 975 ),
679 976 'datatables-counter-column' => array(
680 - 'name' => __( 'Counter Column', 'tablepress' ),
681 - 'description' => __( 'Make the first column an index or counter column with the row position.', 'tablepress' ),
682 - 'category' => 'frontend',
683 - 'class' => 'TablePress_Module_DataTables_Counter_Column',
684 - 'incompatible_classes' => array(),
685 - 'minimum_plan' => 'pro',
686 - 'default_active' => false,
977 + 'name' => __( 'Index Column', 'tablepress' ),
978 + 'description' => __( 'Make the first column an index or counter column with the row position.', 'tablepress' ),
687 979 ),
688 980 'datatables-fixedheader-fixedcolumns' => array(
689 - 'name' => __( 'Fixed Rows and Columns', 'tablepress' ),
690 - 'description' => __( 'Fix the header and footer row and the first and last column when scrolling the table.', 'tablepress' ),
691 - 'category' => 'frontend',
692 - 'class' => 'TablePress_Module_DataTables_FixedHeader_FixedColumns',
693 - 'incompatible_classes' => array(
694 - 'TablePress_DataTables_FixedHeader',
695 - 'TablePress_DataTables_FixedColumns',
696 - ),
697 - 'minimum_plan' => 'pro',
698 - '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' ),
699 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 + ),
700 1000 'datatables-rowgroup' => array(
701 - 'name' => __( 'Row Grouping', 'tablepress' ),
702 - 'description' => __( 'Group table rows by a common keyword, category, or title.', 'tablepress' ),
703 - 'category' => 'frontend',
704 - 'class' => 'TablePress_Module_DataTables_RowGroup',
705 - 'incompatible_classes' => array( 'TablePress_DataTables_RowGroup' ),
706 - 'minimum_plan' => 'pro',
707 - 'default_active' => false,
1001 + 'name' => __( 'Row Grouping', 'tablepress' ),
1002 + 'description' => __( 'Group table rows by a common keyword, category, or title.', 'tablepress' ),
708 1003 ),
709 1004 'datatables-searchbuilder' => array(
710 - 'name' => __( 'Custom Search Builder', 'tablepress' ),
711 - 'description' => __( 'Show a search builder interface for filtering from groups and using conditions.', 'tablepress' ),
712 - 'category' => 'search-filter',
713 - 'class' => 'TablePress_Module_DataTables_SearchBuilder',
714 - 'incompatible_classes' => array(),
715 - 'minimum_plan' => 'max',
716 - 'default_active' => false,
1005 + 'name' => __( 'Custom Search Builder', 'tablepress' ),
1006 + 'description' => __( 'Show a search builder interface for filtering from groups and using conditions.', 'tablepress' ),
717 1007 ),
718 1008 'datatables-searchhighlight' => array(
719 - 'name' => __( 'Search Highlighting', 'tablepress' ),
720 - 'description' => __( 'Highlight found search terms in the table.', 'tablepress' ),
721 - 'category' => 'search-filter',
722 - 'class' => 'TablePress_Module_DataTables_SearchHighlight',
723 - 'incompatible_classes' => array(),
724 - 'minimum_plan' => 'pro',
725 - 'default_active' => false,
1009 + 'name' => __( 'Search Highlighting', 'tablepress' ),
1010 + 'description' => __( 'Highlight found search terms in the table.', 'tablepress' ),
726 1011 ),
727 1012 'datatables-searchpanes' => array(
728 - 'name' => __( 'Search Panes', 'tablepress' ),
729 - 'description' => __( 'Show panes for filtering the columns.', 'tablepress' ),
730 - 'category' => 'search-filter',
731 - 'class' => 'TablePress_Module_DataTables_SearchPanes',
732 - 'incompatible_classes' => array(),
733 - 'minimum_plan' => 'pro',
734 - 'default_active' => false,
1013 + 'name' => __( 'Search Panes', 'tablepress' ),
1014 + 'description' => __( 'Show panes for filtering the columns.', 'tablepress' ),
735 1015 ),
736 1016 'datatables-serverside-processing' => array(
737 - 'name' => __( 'Server-side Processing', 'tablepress' ),
738 - 'description' => __( 'Process sorting, filtering, and pagination on the server for faster loading of large tables.', 'tablepress' ),
739 - 'category' => 'backend',
740 - 'class' => 'TablePress_Module_DataTables_ServerSide_Processing',
741 - 'incompatible_classes' => array(),
742 - 'minimum_plan' => 'max',
743 - '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' ),
744 1019 ),
1020 + 'default-style-customizer' => array(
1021 + 'name' => __( 'Default Style Customizer', 'tablepress' ),
1022 + 'description' => __( 'Change the default styling of your tables in the visual style customizer.', 'tablepress' ),
1023 + ),
1024 + 'email-notifications' => array(
1025 + 'name' => __( 'Email Notifications', 'tablepress' ),
1026 + 'description' => __( 'Get email notifications when certain actions are performed on tables.', 'tablepress' ),
1027 + ),
745 1028 'responsive-tables' => array(
746 - 'name' => __( 'Responsive Tables', 'tablepress' ),
747 - 'description' => __( 'Make your tables look good on different screen sizes.', 'tablepress' ),
748 - 'category' => 'frontend',
749 - 'class' => 'TablePress_Module_Responsive_Tables',
750 - 'incompatible_classes' => array( 'TablePress_Responsive_Tables' ),
751 - 'minimum_plan' => 'pro',
752 - 'default_active' => true,
1029 + 'name' => __( 'Responsive Tables', 'tablepress' ),
1030 + 'description' => __( 'Make your tables look good on different screen sizes.', 'tablepress' ),
753 1031 ),
754 1032 'rest-api' => array(
755 - 'name' => __( 'REST API', 'tablepress' ),
756 - 'description' => __( 'Read table data via the WordPress REST API, e.g. in external apps.', 'tablepress' ),
757 - 'category' => 'backend',
758 - 'class' => 'TablePress_Module_REST_API',
759 - 'incompatible_classes' => array( 'TablePress_REST_API_Controller' ),
760 - 'minimum_plan' => 'max',
761 - 'default_active' => false,
1033 + 'name' => __( 'REST API', 'tablepress' ),
1034 + 'description' => __( 'Read table data via the WordPress REST API, e.g. in external apps.', 'tablepress' ),
762 1035 ),
763 1036 'row-filtering' => array(
764 - 'name' => __( 'Row Filtering', 'tablepress' ),
765 - 'description' => __( 'Show only table rows that contain defined keywords.', 'tablepress' ),
766 - 'category' => 'data-management',
767 - 'class' => 'TablePress_Module_Row_Filtering',
768 - 'incompatible_classes' => array( 'TablePress_Row_Filter' ),
769 - 'minimum_plan' => 'pro',
770 - 'default_active' => true,
1037 + 'name' => __( 'Row Filtering', 'tablepress' ),
1038 + 'description' => __( 'Show only table rows that contain defined keywords.', 'tablepress' ),
771 1039 ),
772 1040 'row-highlighting' => array(
773 - 'name' => __( 'Row Highlighting', 'tablepress' ),
774 - 'description' => __( 'Add CSS classes to rows for highlighting based on their content.', 'tablepress' ),
775 - 'category' => 'frontend',
776 - 'class' => 'TablePress_Module_Row_Highlighting',
777 - 'incompatible_classes' => array( 'TablePress_Row_Highlighting' ),
778 - 'minimum_plan' => 'pro',
779 - 'default_active' => false,
1041 + 'name' => __( 'Row Highlighting', 'tablepress' ),
1042 + 'description' => __( 'Add CSS classes to rows for highlighting based on their content.', 'tablepress' ),
780 1043 ),
781 1044 'row-order' => array(
782 - 'name' => __( 'Row Order', 'tablepress' ),
783 - 'description' => __( 'Order the rows in different ways when a table is shown.', 'tablepress' ),
784 - 'category' => 'data-management',
785 - 'class' => 'TablePress_Module_Row_Order',
786 - 'incompatible_classes' => array( 'TablePress_Row_Order' ),
787 - 'minimum_plan' => 'pro',
788 - 'default_active' => false,
1045 + 'name' => __( 'Row Order', 'tablepress' ),
1046 + 'description' => __( 'Order the rows in different ways when a table is shown.', 'tablepress' ),
789 1047 ),
790 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 + }
791 1127 }
792 1128
793 1129 } // class TablePress