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 +762 -109 1.12 → 3.4 View file →
@@ -6,13 +6,16 @@
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 /**
14 16 * TablePress class
17 + *
15 18 * @package TablePress
16 19 * @author Tobias Bäthge
17 20 * @since 1.0.0
18 21 */
@@ -25,9 +28,9 @@
25 28 *
26 29 * @since 1.0.0
27 30 * @const string
28 31 */
29 - const version = '1.12';
32 + public const version = '3.4'; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
30 33
31 34 /**
32 35 * TablePress internal plugin version ("options scheme" version).
33 36 *
@@ -35,9 +38,9 @@
35 38 *
36 39 * @since 1.0.0
37 40 * @const int
38 41 */
39 - const db_version = 41;
42 + public const db_version = 133; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
40 43
41 44 /**
42 45 * TablePress "table scheme" (data format structure) version.
43 46 *
@@ -46,33 +49,30 @@
46 49 *
47 50 * @since 1.0.0
48 51 * @const int
49 52 */
50 - const table_scheme_version = 3;
53 + public const table_scheme_version = 3; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
51 54
52 55 /**
53 56 * Instance of the Options Model.
54 57 *
55 58 * @since 1.3.0
56 - * @var TablePress_Options_Model
57 59 */
58 - public static $model_options;
60 + public static \TablePress_Options_Model $model_options;
59 61
60 62 /**
61 63 * Instance of the Table Model.
62 64 *
63 65 * @since 1.3.0
64 - * @var TablePress_Table_Model
65 66 */
66 - public static $model_table;
67 + public static \TablePress_Table_Model $model_table;
67 68
68 69 /**
69 70 * Instance of the controller.
70 71 *
71 72 * @since 1.0.0
72 - * @var TablePress_*_Controller
73 73 */
74 - public static $controller;
74 + public static \TablePress_Frontend_Controller $controller;
75 75
76 76 /**
77 77 * Name of the Shortcode to show a TablePress table.
78 78 *
@@ -78,11 +78,11 @@
78 78 *
79 79 * Should only be modified through the filter hook 'tablepress_table_shortcode'.
80 80 *
81 81 * @since 1.0.0
82 - * @var string
82 + * @var non-empty-string
83 83 */
84 - public static $shortcode = 'table';
84 + public static string $shortcode = 'table';
85 85
86 86 /**
87 87 * Name of the Shortcode to show extra information of a TablePress table.
88 88 *
@@ -88,57 +88,49 @@
88 88 *
89 89 * Should only be modified through the filter hook 'tablepress_table_info_shortcode'.
90 90 *
91 91 * @since 1.0.0
92 - * @var string
92 + * @var non-empty-string
93 93 */
94 - public static $shortcode_info = 'table-info';
94 + public static string $shortcode_info = 'table-info';
95 95
96 96 /**
97 + * List of TablePress premium modules.
98 + *
99 + * @since 2.1.0
100 + * @var array<string, array<string, mixed>> $modules Array with module slugs as keys and module data as values.
101 + */
102 + public static array $modules = array();
103 +
104 + /**
97 105 * Start-up TablePress (run on WordPress "init") and load the controller for the current state.
98 106 *
99 107 * @since 1.0.0
100 108 */
101 - public static function run() {
109 + public static function run(): void {
102 110 /**
103 - * Fires when TablePress is loaded.
111 + * Fires before TablePress is loaded.
104 112 *
113 + * The `tablepress_loaded` action hook might be a better choice in most situations, as TablePress options will then be available.
114 + *
105 115 * @since 1.0.0
106 116 */
107 117 do_action( 'tablepress_run' );
108 118
109 - // Exit early if TablePress doesn't have to be loaded.
110 - if ( ( 'wp-login.php' === basename( $_SERVER['SCRIPT_FILENAME'] ) ) // Login screen
111 - || ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
112 - || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) {
113 - return;
114 - }
115 -
116 - // Check if minimum requirements are fulfilled, currently WordPress 5.3.
117 - include( ABSPATH . WPINC . '/version.php' ); // Include an unmodified $wp_version.
118 - if ( version_compare( str_replace( '-src', '', $wp_version ), '5.3', '<' ) ) {
119 - // Show error notice to admins, if WP is not installed in the minimum required version, in which case TablePress will not work.
120 - if ( current_user_can( 'update_plugins' ) ) {
121 - add_action( 'admin_notices', array( 'TablePress', 'show_minimum_requirements_error_notice' ) );
122 - }
123 - // And exit TablePress.
124 - return;
125 - }
126 -
127 119 /**
128 - * Filter the string that is used as the [table] Shortcode.
120 + * Filters the string that is used as the [table] Shortcode.
129 121 *
130 122 * @since 1.0.0
131 123 *
132 - * @param string $shortcode The [table] Shortcode string.
124 + * @param non-empty-string $shortcode The [table] Shortcode string.
133 125 */
134 126 self::$shortcode = apply_filters( 'tablepress_table_shortcode', self::$shortcode );
135 127 /**
136 - * Filter the string that is used as the [table-info] Shortcode.
128 + * Filters the string that is used as the [table-info] Shortcode.
137 129 *
138 130 * @since 1.0.0
139 131 *
140 - * @param string $shortcode_info The [table-info] Shortcode string.
132 + * @param non-empty-string $shortcode_info The [table-info] Shortcode string.
141 133 */
142 134 self::$shortcode_info = apply_filters( 'tablepress_table_info_shortcode', self::$shortcode_info );
143 135
144 136 // Load modals for table and options, to be accessible from everywhere via `TablePress::$model_options` and `TablePress::$model_table`.
@@ -144,17 +136,49 @@
144 136 // Load modals for table and options, to be accessible from everywhere via `TablePress::$model_options` and `TablePress::$model_table`.
145 137 self::$model_options = self::load_model( 'options' );
146 138 self::$model_table = self::load_model( 'table' );
147 139
140 + // Exit early, i.e. before a controller is loaded, if TablePress functionality is likely not needed.
141 + $exit_early = false;
142 + if ( ( isset( $_SERVER['SCRIPT_FILENAME'] ) && 'wp-login.php' === basename( $_SERVER['SCRIPT_FILENAME'] ) ) // Detect the WordPress Login screen.
143 + || ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
144 + || wp_doing_cron() ) {
145 + $exit_early = true;
146 + }
147 + /**
148 + * Filters whether TablePress should exit early, e.g. during wp-login.php, XML-RPC, and WP-Cron requests.
149 + *
150 + * @since 2.0.0
151 + *
152 + * @param bool $exit_early Whether TablePress should exit early.
153 + */
154 + if ( apply_filters( 'tablepress_exit_early', $exit_early ) ) {
155 + return;
156 + }
157 +
148 158 if ( is_admin() ) {
149 159 $controller = 'admin';
150 160 if ( wp_doing_ajax() ) {
151 - $controller .= '_ajax';
161 + $controller = 'admin_ajax';
152 162 }
153 - } else {
154 - $controller = 'frontend';
163 + self::load_controller( $controller );
155 164 }
156 - self::$controller = self::load_controller( $controller );
165 + // Load the frontend controller in all scenarios, so that Shortcode render functions are always available.
166 + self::$controller = self::load_controller( 'frontend' );
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 +
173 + /**
174 + * Fires after TablePress is loaded.
175 + *
176 + * The `tablepress_run` action hook can be used if code has to run before TablePress is loaded.
177 + *
178 + * @since 2.0.0
179 + */
180 + do_action( 'tablepress_loaded' );
157 181 }
158 182
159 183 /**
160 184 * Load a file with require_once(), after running it through a filter.
@@ -160,15 +184,15 @@
160 184 * Load a file with require_once(), after running it through a filter.
161 185 *
162 186 * @since 1.0.0
163 187 *
164 - * @param string $file Name of the PHP file with the class.
165 - * @param string $folder Name of the folder with $class's $file.
188 + * @param string $file Name of the PHP file.
189 + * @param string $folder Name of the folder with the file.
166 190 */
167 - public static function load_file( $file, $folder ) {
191 + public static function load_file( string $file, string $folder ): void {
168 192 $full_path = TABLEPRESS_ABSPATH . $folder . '/' . $file;
169 193 /**
170 - * Filter the full path of a file that shall be loaded.
194 + * Filters the full path of a file that shall be loaded.
171 195 *
172 196 * @since 1.0.0
173 197 *
174 198 * @param string $full_path Full path of the file that shall be loaded.
@@ -181,32 +205,32 @@
181 205 }
182 206 }
183 207
184 208 /**
185 - * Create a new instance of the $class, which is stored in $file in the $folder subfolder
209 + * Create a new instance of the $class_name, which is stored in $file in the $folder subfolder
186 210 * of the plugin's directory.
187 211 *
188 212 * @since 1.0.0
189 213 *
190 - * @param string $class Name of the class.
191 - * @param string $file Name of the PHP file with the class.
192 - * @param string $folder Name of the folder with $class's $file.
193 - * @param mixed $params Optional. Parameters that are passed to the constructor of $class.
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.
194 218 * @return object Initialized instance of the class.
195 219 */
196 - public static function load_class( $class, $file, $folder, $params = null ) {
220 + public static function load_class( string $class_name, string $file, string $folder, /* ?array|string */ $params = null ): object {
197 221 /**
198 - * Filter name of the class that shall be loaded.
222 + * Filters name of the class that shall be loaded.
199 223 *
200 224 * @since 1.0.0
201 225 *
202 - * @param string $class Name of the class that shall be loaded.
226 + * @param string $class_name Name of the class that shall be loaded.
203 227 */
204 - $class = apply_filters( 'tablepress_load_class_name', $class );
205 - if ( ! class_exists( $class, false ) ) {
228 + $class_name = apply_filters( 'tablepress_load_class_name', $class_name );
229 + if ( ! class_exists( $class_name, false ) ) {
206 230 self::load_file( $file, $folder );
207 231 }
208 - $the_class = new $class( $params );
232 + $the_class = new $class_name( $params );
209 233 return $the_class;
210 234 }
211 235
212 236 /**
@@ -216,9 +240,9 @@
216 240 *
217 241 * @param string $model Name of the model.
218 242 * @return object Instance of the initialized model.
219 243 */
220 - public static function load_model( $model ) {
244 + public static function load_model( string $model ): object {
221 245 // Model Base Class.
222 246 self::load_file( 'class-model.php', 'classes' );
223 247 // Make first letter uppercase for a better looking naming pattern.
224 248 $ucmodel = ucfirst( $model );
@@ -230,13 +254,13 @@
230 254 * Create a new instance of the $view, which is stored in the "views" subfolder, and set it up with $data.
231 255 *
232 256 * @since 1.0.0
233 257 *
234 - * @param string $view Name of the view to load.
235 - * @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.
236 260 * @return object Instance of the initialized view, already set up, just needs to be rendered.
237 261 */
238 - public static function load_view( $view, array $data = array() ) {
262 + public static function load_view( string $view, array $data = array() ): object {
239 263 // View Base Class.
240 264 self::load_file( 'class-view.php', 'classes' );
241 265 // Make first letter uppercase for a better looking naming pattern.
242 266 $ucview = ucfirst( $view );
@@ -252,9 +276,9 @@
252 276 *
253 277 * @param string $controller Name of the controller.
254 278 * @return object Instance of the initialized controller.
255 279 */
256 - public static function load_controller( $controller ) {
280 + public static function load_controller( string $controller ): object {
257 281 // Controller Base Class.
258 282 self::load_file( 'class-controller.php', 'classes' );
259 283 // Make first letter uppercase for a better looking naming pattern.
260 284 $uccontroller = ucfirst( $controller );
@@ -266,13 +290,13 @@
266 290 * Generate the complete nonce string, from the nonce base, the action and an item, e.g. tablepress_delete_table_3.
267 291 *
268 292 * @since 1.0.0
269 293 *
270 - * @param string $action Action for which the nonce is needed.
271 - * @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.
272 296 * @return string The resulting nonce string.
273 297 */
274 - public static function nonce( $action, $item = false ) {
298 + public static function nonce( string $action, /* string|false */ $item = false ): string {
275 299 $nonce = "tablepress_{$action}";
276 300 if ( $item ) {
277 301 $nonce .= "_{$item}";
278 302 }
@@ -283,14 +307,14 @@
283 307 * Check whether a nonce string is valid.
284 308 *
285 309 * @since 1.0.0
286 310 *
287 - * @param string $action Action for which the nonce should be checked.
288 - * @param string|bool $item Optional. Item for which the action should be performed, like "table".
289 - * @param string $query_arg Optional. Name of the nonce query string argument in $_POST.
290 - * @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.
291 315 */
292 - 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 {
293 317 $nonce_action = self::nonce( $action, $item );
294 318 if ( $ajax ) {
295 319 check_ajax_referer( $nonce_action, $query_arg );
296 320 } else {
@@ -305,16 +329,17 @@
305 329 *
306 330 * @since 1.0.0
307 331 *
308 332 * @param string $column Column string.
309 - * @return int $number Column number, 1-based.
333 + * @return int Column number, 1-based.
310 334 */
311 - 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 );
312 337 $column = strtoupper( $column );
313 338 $count = strlen( $column );
314 339 $number = 0;
315 340 for ( $i = 0; $i < $count; $i++ ) {
316 - $number += ( ord( $column[ $count - 1 - $i ] ) - 64 ) * pow( 26, $i );
341 + $number += ( ord( $column[ $count - 1 - $i ] ) - 64 ) * 26 ** $i;
317 342 }
318 343 return $number;
319 344 }
320 345
@@ -325,36 +350,145 @@
325 350 *
326 351 * @since 1.0.0
327 352 *
328 353 * @param int $number Column number, 1-based.
329 - * @return string $column Column string.
354 + * @return string Column string.
330 355 */
331 - public static function number_to_letter( $number ) {
356 + public static function number_to_letter( int $number ): string {
332 357 $column = '';
333 358 while ( $number > 0 ) {
334 359 $column = chr( 65 + ( ( $number - 1 ) % 26 ) ) . $column;
335 - $number = floor( ( $number - 1 ) / 26 );
360 + $number = intdiv( $number - 1, 26 );
336 361 }
337 362 return $column;
338 363 }
339 364
340 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 + /**
341 460 * Get a nice looking date and time string from the mySQL format of datetime strings for output.
342 461 *
343 462 * @since 1.0.0
344 463 *
345 - * @param string $datetime DateTime string in mySQL format or a Unix timestamp.
346 - * @param string $type Optional. Type of $datetime, 'mysql' or 'timestamp'.
347 - * @param string $separator Optional. Separator between date and time.
464 + * @param string $datetime_string DateTime string, often in mySQL format..
465 + * @param string $separator_or_format Optional. Separator between date and time, or format string.
348 466 * @return string Nice looking string with the date and time.
349 467 */
350 - public static function format_datetime( $datetime, $type = 'mysql', $separator = ' ' ) {
351 - // @TODO: Maybe change from using the stored WP Options to translated date/time schemes, like in https://core.trac.wordpress.org/changeset/35811.
352 - if ( 'mysql' === $type ) {
353 - return mysql2date( get_option( 'date_format' ), $datetime ) . $separator . mysql2date( get_option( 'time_format' ), $datetime );
354 - } else {
355 - return date_i18n( get_option( 'date_format' ), $datetime ) . $separator . date_i18n( get_option( 'time_format' ), $datetime );
468 + public static function format_datetime( string $datetime_string, string $separator_or_format = ' ' ): string {
469 + $timezone = wp_timezone();
470 + $datetime = date_create( $datetime_string, $timezone );
471 + if ( false === $datetime ) {
472 + return $datetime_string;
356 473 }
474 + $timestamp = $datetime->getTimestamp();
475 +
476 + switch ( $separator_or_format ) {
477 + case ' ':
478 + case '<br />':
479 + case '<br/>':
480 + case '<br>':
481 + $date = wp_date( get_option( 'date_format' ), $timestamp, $timezone );
482 + $time = wp_date( get_option( 'time_format' ), $timestamp, $timezone );
483 + $output = "{$date}{$separator_or_format}{$time}";
484 + break;
485 + default:
486 + $output = (string) wp_date( $separator_or_format, $timestamp, $timezone );
487 + break;
488 + }
489 +
490 + return $output;
357 491 }
358 492
359 493 /**
360 494 * Get the name from a WP user ID (used to store information on last editor of a table).
@@ -363,11 +497,12 @@
363 497 *
364 498 * @param int $user_id WP user ID.
365 499 * @return string Nickname of the WP user with the $user_id.
366 500 */
367 - public static function get_user_display_name( $user_id ) {
501 + public static function get_user_display_name( int $user_id ): string {
368 502 $user = get_userdata( $user_id );
369 - return ( $user && 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' ) );
370 505 }
371 506
372 507 /**
373 508 * Sanitizes a CSS class to ensure it only contains valid characters.
@@ -376,31 +511,312 @@
376 511 * This is an extension to WP's `sanitize_html_class()`, to also allow `:` which are used in some CSS frameworks.
377 512 *
378 513 * @since 1.11.0
379 514 *
380 - * @param string $class The CSS class name to be sanitized.
515 + * @param string $css_class The CSS class name to be sanitized.
381 516 * @return string The sanitized CSS class.
382 517 */
383 - public static function sanitize_css_class( $class ) {
518 + public static function sanitize_css_class( string $css_class ): string {
384 519 // Strip out any %-encoded octets.
385 - $sanitized_class = preg_replace( '|%[a-fA-F0-9][a-fA-F0-9]|', '', $class );
520 + $sanitized_css_class = (string) preg_replace( '|%[a-fA-F0-9][a-fA-F0-9]|', '', $css_class );
386 521 // Limit to A-Z, a-z, 0-9, ':', '_', and '-'.
387 - $sanitized_class = preg_replace( '/[^A-Za-z0-9:_-]/', '', $sanitized_class );
388 - return $sanitized_class;
522 + $sanitized_css_class = (string) preg_replace( '/[^A-Za-z0-9:_-]/', '', $sanitized_css_class );
523 + return $sanitized_css_class;
389 524 }
390 525
391 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 + /**
775 + * Retrieves all information of a WP_Error object as a string.
776 + *
777 + * @since 1.4.0
778 + *
779 + * @param WP_Error $wp_error A WP_Error object.
780 + * @return string All error codes, messages, and data of the WP_Error.
781 + */
782 + public static function get_wp_error_string( WP_Error $wp_error ): string {
783 + $error_strings = array();
784 + $error_codes = $wp_error->get_error_codes();
785 + // Reverse order to get latest errors first.
786 + $error_codes = array_reverse( $error_codes );
787 + foreach ( $error_codes as $error_code ) {
788 + $error_strings[ $error_code ] = $error_code;
789 + $error_messages = $wp_error->get_error_messages( $error_code );
790 + $error_messages = implode( ', ', $error_messages );
791 + if ( ! empty( $error_messages ) ) {
792 + $error_strings[ $error_code ] .= " ({$error_messages})";
793 + }
794 + $error_data = $wp_error->get_error_data( $error_code );
795 + if ( is_string( $error_data ) ) {
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}]";
803 + }
804 + }
805 + return implode( ";\n", $error_strings );
806 + }
807 +
808 + /**
392 809 * Generate the action URL, to be used as a link within the plugin (e.g. in the submenu navigation or List of Tables).
393 810 *
394 811 * @since 1.0.0
395 812 *
396 - * @param array $params Optional. Parameters to form the query string of the URL.
397 - * @param bool $add_nonce Optional. Whether the URL shall be nonced by WordPress.
398 - * @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.
399 816 * @return string The URL for the given parameters (already run through esc_url() with $add_nonce === true!).
400 817 */
401 - public static function url( array $params = array(), $add_nonce = false, $target = '' ) {
402 -
818 + public static function url( array $params = array(), bool $add_nonce = false, string $target = '' ): string {
403 819 // Default action is "list", if no action given.
404 820 if ( ! isset( $params['action'] ) ) {
405 821 $params['action'] = 'list';
406 822 }
@@ -405,9 +821,9 @@
405 821 $params['action'] = 'list';
406 822 }
407 823 $nonce_action = $params['action'];
408 824
409 - if ( $target ) {
825 + if ( '' !== $target ) {
410 826 $params['action'] = "tablepress_{$params['action']}";
411 827 } else {
412 828 $params['page'] = 'tablepress';
413 829 // Top-level parent page needs special treatment for better action strings.
@@ -431,11 +847,15 @@
431 847 'item' => false,
432 848 );
433 849 $params = array_merge( $default_params, $params );
434 850
851 + if ( isset( $params['error_details'] ) ) {
852 + $params['error_details'] = rawurlencode( $params['error_details'] );
853 + }
854 +
435 855 $url = add_query_arg( $params, admin_url( $target ) );
436 856 if ( $add_nonce ) {
437 - $url = wp_nonce_url( $url, self::nonce( $nonce_action, $params['item'] ) ); // wp_nonce_url() does esc_html()
857 + $url = wp_nonce_url( $url, self::nonce( $nonce_action, $params['item'] ) ); // wp_nonce_url() does esc_html().
438 858 }
439 859 return $url;
440 860 }
441 861
@@ -443,12 +863,12 @@
443 863 * Create a redirect URL from the $target_parameters and redirect the user.
444 864 *
445 865 * @since 1.0.0
446 866 *
447 - * @param array $params Optional. Parameters from which the target URL is constructed.
448 - * @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.
449 869 */
450 - public static function redirect( array $params = array(), $add_nonce = false ) {
870 + public static function redirect( array $params = array(), bool $add_nonce = false ): void {
451 871 $redirect = self::url( $params );
452 872 if ( $add_nonce ) {
453 873 if ( ! isset( $params['item'] ) ) {
454 874 $params['item'] = false;
@@ -460,17 +880,250 @@
460 880 exit;
461 881 }
462 882
463 883 /**
464 - * 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.
465 885 *
466 - * @since 1.0.0
886 + * @since 3.1.0
887 + *
888 + * @return string The editor that the site uses, either "block", "elementor", or "other".
467 889 */
468 - public static function show_minimum_requirements_error_notice() {
469 - // Message is not translated as it is shown on every admin screen, for which we don't want to load translations.
470 - echo '<div class="notice notice-error form-invalid"><p>' .
471 - '<strong>Attention:</strong> ' .
472 - '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>!' .
473 - "</p></div>\n";
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.
896 + $site_uses_block_editor = use_block_editor_for_post_type( 'post' )
897 + && ! is_plugin_active( 'classic-editor/classic-editor.php' )
898 + && ! is_plugin_active( 'classic-editor-addon/classic-editor-addon.php' )
899 + && ! is_plugin_active( 'siteorigin-panels/siteorigin-panels.php' )
900 + && ! is_plugin_active( 'beaver-builder-lite-version/fl-builder.php' );
901 + /**
902 + * Filters the outcome of the check whether the site uses the block editor.
903 + *
904 + * This can be used when certain conditions (e.g. new site builders) are not (yet) accounted for.
905 + *
906 + * @since 2.0.1
907 + *
908 + * @param bool $site_uses_block_editor True if the site uses the block editor, false otherwise.
909 + */
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 + }
914 +
915 + return 'other';
916 + }
917 +
918 + /**
919 + * Adds the (translated) names and descriptions to the list of feature modules.
920 + *
921 + * @since 3.3.0
922 + */
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(
932 + 'advanced-access-rights' => array(
933 + 'name' => __( 'Advanced Access Rights', 'tablepress' ),
934 + 'description' => __( 'Restrict access to individual tables for individual users.', 'tablepress' ),
935 + ),
936 + 'automatic-periodic-table-import' => array(
937 + 'name' => __( 'Automatic Periodic Table Import', 'tablepress' ),
938 + 'description' => __( 'Periodically update tables from a configured import source.', 'tablepress' ),
939 + ),
940 + 'automatic-table-export' => array(
941 + 'name' => __( 'Automatic Table Export', 'tablepress' ),
942 + 'description' => __( 'Export and save tables to files on the server after they were modified.', 'tablepress' ),
943 + ),
944 + 'cell-highlighting' => array(
945 + 'name' => __( 'Cell Highlighting', 'tablepress' ),
946 + 'description' => __( 'Add CSS classes to cells for highlighting based on their content.', 'tablepress' ),
947 + ),
948 + 'column-order' => array(
949 + 'name' => __( 'Column Order', 'tablepress' ),
950 + 'description' => __( 'Order the columns in different ways when a table is shown.', 'tablepress' ),
951 + ),
952 + 'datatables-advanced-loading' => array(
953 + 'name' => __( 'Advanced Loading', 'tablepress' ),
954 + 'description' => __( 'Load the table data from a JSON array for faster loading.', 'tablepress' ),
955 + ),
956 + 'datatables-alphabetsearch' => array(
957 + 'name' => __( 'Alphabet Search', 'tablepress' ),
958 + 'description' => __( 'Show Alphabet buttons above the table to filter rows by their first letter.', 'tablepress' ),
959 + ),
960 + 'datatables-auto-filter' => array(
961 + 'name' => __( 'Automatic Filter', 'tablepress' ),
962 + 'description' => __( 'Pre-filter a table when it is shown.', 'tablepress' ),
963 + ),
964 + 'datatables-buttons' => array(
965 + 'name' => __( 'User Action Buttons', 'tablepress' ),
966 + 'description' => __( 'Add buttons for downloading, copying, printing, and changing column visibility of tables.', 'tablepress' ),
967 + ),
968 + 'datatables-columnfilterwidgets' => array(
969 + 'name' => __( 'Column Filter Dropdowns', 'tablepress' ),
970 + 'description' => __( 'Add a search dropdown for each column above the table.', 'tablepress' ),
971 + ),
972 + 'datatables-column-filter' => array(
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' ),
975 + ),
976 + 'datatables-counter-column' => array(
977 + 'name' => __( 'Index Column', 'tablepress' ),
978 + 'description' => __( 'Make the first column an index or counter column with the row position.', 'tablepress' ),
979 + ),
980 + 'datatables-fixedheader-fixedcolumns' => array(
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' ),
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 + ),
1000 + 'datatables-rowgroup' => array(
1001 + 'name' => __( 'Row Grouping', 'tablepress' ),
1002 + 'description' => __( 'Group table rows by a common keyword, category, or title.', 'tablepress' ),
1003 + ),
1004 + 'datatables-searchbuilder' => array(
1005 + 'name' => __( 'Custom Search Builder', 'tablepress' ),
1006 + 'description' => __( 'Show a search builder interface for filtering from groups and using conditions.', 'tablepress' ),
1007 + ),
1008 + 'datatables-searchhighlight' => array(
1009 + 'name' => __( 'Search Highlighting', 'tablepress' ),
1010 + 'description' => __( 'Highlight found search terms in the table.', 'tablepress' ),
1011 + ),
1012 + 'datatables-searchpanes' => array(
1013 + 'name' => __( 'Search Panes', 'tablepress' ),
1014 + 'description' => __( 'Show panes for filtering the columns.', 'tablepress' ),
1015 + ),
1016 + 'datatables-serverside-processing' => array(
1017 + 'name' => __( 'Server-side Processing', 'tablepress' ),
1018 + 'description' => __( 'Process sorting, filtering, and pagination on the server for faster loading of large tables.', 'tablepress' ),
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 + ),
1028 + 'responsive-tables' => array(
1029 + 'name' => __( 'Responsive Tables', 'tablepress' ),
1030 + 'description' => __( 'Make your tables look good on different screen sizes.', 'tablepress' ),
1031 + ),
1032 + 'rest-api' => array(
1033 + 'name' => __( 'REST API', 'tablepress' ),
1034 + 'description' => __( 'Read table data via the WordPress REST API, e.g. in external apps.', 'tablepress' ),
1035 + ),
1036 + 'row-filtering' => array(
1037 + 'name' => __( 'Row Filtering', 'tablepress' ),
1038 + 'description' => __( 'Show only table rows that contain defined keywords.', 'tablepress' ),
1039 + ),
1040 + 'row-highlighting' => array(
1041 + 'name' => __( 'Row Highlighting', 'tablepress' ),
1042 + 'description' => __( 'Add CSS classes to rows for highlighting based on their content.', 'tablepress' ),
1043 + ),
1044 + 'row-order' => array(
1045 + 'name' => __( 'Row Order', 'tablepress' ),
1046 + 'description' => __( 'Order the rows in different ways when a table is shown.', 'tablepress' ),
1047 + ),
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 + }
474 1127 }
475 1128
476 1129 } // class TablePress