Skip to content

Commit 62aab50

Browse files
committed
Finish TablePress 2.2.5.
1 parent 1030008 commit 62aab50

29 files changed

+186
-71
lines changed

Diff for: .gitattributes

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
/.git/ export-ignore
55
/.github/ export-ignore
66
/.phpstan/ export-ignore
7+
/.wordpress-org/ export-ignore
78
/node_modules/ export-ignore
89
/tests/ export-ignore
910
/vendor/ export-ignore
@@ -13,20 +14,23 @@
1314
/.eslintrc.js export-ignore
1415
/.gitattributes export-ignore
1516
/.gitignore export-ignore
17+
/.phpunit.result.cache export-ignore
1618
/.prettierignore export-ignore
1719
/.prettierrc.js export-ignore
1820
/.stylelintignore export-ignore
1921
/.stylelintrc.json export-ignore
22+
/.typos.toml export-ignore
2023
/composer.json export-ignore
2124
/composer.lock export-ignore
2225
/Gruntfile.js export-ignore
23-
/package.json export-ignore
2426
/package-lock.json export-ignore
27+
/package.json export-ignore
2528
/phpcompat.xml.dist export-ignore
2629
/phpcs.xml.dist export-ignore
2730
/phpstan.neon.dist export-ignore
2831
/phpunit.xml.dist export-ignore
2932
/readme.md export-ignore
33+
/rector.php export-ignore
3034
/webpack.config.js export-ignore
3135

3236
# Set default behaviour, in case users don't have core.autocrlf set.

Diff for: .github/workflows/wp-org-deploy.yml

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Deploy to wordpress.org Repository
2+
3+
on:
4+
# Deploy to wordpress.org when a production release is created.
5+
release:
6+
types: [ released ]
7+
# Do a "dry run" (without comming to SVN) when the workflow is manually triggered.
8+
workflow_dispatch:
9+
10+
jobs:
11+
12+
deploy_to_wordpress_org:
13+
name: Deploy to wordpress.org${{ github.event_name == 'workflow_dispatch' && ' (dry run)' || '' }}
14+
if: github.repository == 'TablePress/TablePress'
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Checkout TablePress repository
19+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
20+
21+
- name: WordPress Plugin Deploy
22+
id: deploy
23+
uses: 10up/action-wordpress-plugin-deploy@stable
24+
with:
25+
generate-zip: true
26+
dry-run: ${{ github.event_name == 'workflow_dispatch' }}
27+
env:
28+
SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
29+
SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
30+
SLUG: ${{ vars.SLUG }}
31+
32+
- name: Get plugin version
33+
id: get-version
34+
if: ${{ github.event_name != 'workflow_dispatch' }}
35+
run: |
36+
echo "version=$(awk '/Stable tag: /{print $NF}' readme.txt)" >> $GITHUB_OUTPUT
37+
38+
- name: Upload release ZIP file
39+
uses: actions/upload-release-asset@v1
40+
if: ${{ github.event_name != 'workflow_dispatch' }}
41+
env:
42+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43+
with:
44+
upload_url: ${{ github.event.release.upload_url }}
45+
asset_path: ${{ steps.deploy.outputs.zip-path }}
46+
asset_name: ${{ vars.SLUG }}.${{ steps.get-version.outputs.version }}.zip
47+
asset_content_type: application/zip

Diff for: admin/js/build/edit.asset.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<?php return array('dependencies' => array('wp-hooks', 'wp-i18n', 'wp-url'), 'version' => 'cdf1475bc9e3883381b2');
1+
<?php return array('dependencies' => array('wp-hooks', 'wp-i18n', 'wp-url'), 'version' => 'aabe698a026e2cdb0743');

Diff for: admin/js/build/edit.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: admin/js/edit.js

+19-9
Original file line numberDiff line numberDiff line change
@@ -765,7 +765,7 @@ tp.callbacks.table_preview.process = function ( event ) {
765765

766766
// Add spinner, disable "Preview" buttons, and change cursor.
767767
event.target.parentNode.insertAdjacentHTML( 'beforeend', `<span id="spinner-table-preview" class="spinner-table-preview spinner is-active" title="${ __( 'The Table Preview is being loaded …', 'tablepress' ) }"/>` );
768-
$( '.button-show-preview' ).forEach( ( button ) => button.classList.add( 'disabled' ) );
768+
$( '.button-preview' ).forEach( ( button ) => button.classList.add( 'disabled' ) );
769769
document.body.classList.add( 'wait' );
770770

771771
// Load the table preview data from the server via an AJAX request.
@@ -800,7 +800,7 @@ tp.callbacks.table_preview.process = function ( event ) {
800800
.catch( ( error ) => tp.callbacks.table_preview.error( error.message ) )
801801
.finally( () => {
802802
$( '#spinner-table-preview' ).remove();
803-
$( '.button-show-preview' ).forEach( ( button ) => button.classList.remove( 'disabled' ) );
803+
$( '.button-preview' ).forEach( ( button ) => button.classList.remove( 'disabled' ) );
804804
document.body.classList.remove( 'wait' );
805805
} );
806806
};
@@ -938,12 +938,22 @@ tp.callbacks.save_changes.success = function ( data ) {
938938
// Update the nonces.
939939
tp.nonces.edit_table = data.new_edit_nonce;
940940
tp.nonces.preview_table = data.new_preview_nonce;
941+
tp.nonces.copy_table = data.new_copy_nonce;
942+
tp.nonces.delete_table = data.new_delete_nonce;
943+
944+
// Update URLs in Preview, Copy, and Delete links/buttons.
945+
[ 'preview', 'copy', 'delete' ].forEach( ( action ) => {
946+
$( `.button-${ action }` ).forEach( ( button ) => {
947+
button.href = button.href
948+
.replace( /item=[a-zA-Z0-9_-]+/g, `item=${ data.table_id }` ) // Updates both the "item" and the "return_item" parameters.
949+
.replace( /&_wpnonce=[a-z0-9]+/ig, `&_wpnonce=${ data[ `new_${ action }_nonce` ] }` );
950+
} );
951+
} );
941952

942-
// Update URLs in Preview links.
943-
$( '.button-show-preview' ).forEach( ( button ) => {
953+
// Update URL in Export links/buttons.
954+
$( '.button-export' ).forEach( ( button ) => {
944955
button.href = button.href
945-
.replace( /item=[a-zA-Z0-9_-]+/g, `item=${ data.table_id }` )
946-
.replace( /&_wpnonce=[a-z0-9]+/ig, `&_wpnonce=${ data.new_preview_nonce }` );
956+
.replace( /table_id=[a-zA-Z0-9_-]+/g, `table_id=${ data.table_id }` );
947957
} );
948958

949959
// Update last-modified date and user nickname.
@@ -1309,7 +1319,7 @@ tp.callbacks.keyboard_shortcuts = function ( event ) {
13091319
if ( event.ctrlKey || event.metaKey ) {
13101320
if ( 80 === event.keyCode ) {
13111321
// Preview: Ctrl/Cmd + P.
1312-
action = 'show-preview';
1322+
action = 'preview';
13131323
} else if ( 83 === event.keyCode ) {
13141324
// Save Changes: Ctrl/Cmd + S.
13151325
action = 'save-changes';
@@ -1365,7 +1375,7 @@ tp.callbacks.keyboard_shortcuts = function ( event ) {
13651375
}
13661376
}
13671377

1368-
if ( 'save-changes' === action || 'show-preview' === action ) {
1378+
if ( 'save-changes' === action || 'preview' === action ) {
13691379
// Blur the focussed element to make sure that all change events were triggered.
13701380
document.activeElement.blur(); // eslint-disable-line @wordpress/no-global-active-element
13711381

@@ -1446,7 +1456,7 @@ $( '#tablepress-page' ).addEventListener( 'click', ( event ) => {
14461456
return;
14471457
}
14481458

1449-
if ( event.target.matches( '.button-show-preview' ) ) {
1459+
if ( event.target.matches( '.button-preview' ) ) {
14501460
tp.callbacks.table_preview.process( event );
14511461
return;
14521462
}

Diff for: blocks/table/block.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://schemas.wp.org/trunk/block.json",
33
"apiVersion": 3,
44
"name": "tablepress/table",
5-
"version": "2.2.4",
5+
"version": "2.2.5",
66
"title": "TablePress table",
77
"category": "media",
88
"icon": "list-view",

Diff for: classes/class-import-phpspreadsheet.php

+13-4
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,12 @@ public function import_table( array $file ) /* : array|WP_Error */ {
7777
* @return array<string, mixed>|false Table array on success, false if the file is not a JSON file.
7878
*/
7979
protected function _maybe_import_json( string $data ) /* : array|false */ {
80-
// If the first non-whitespace character is not a { or [, the file is not a supported JSON file.
81-
$data = ltrim( $data );
80+
$data = trim( $data );
81+
82+
// If the file does not begin / end with [ / ] or { / }, it's not a supported JSON file.
8283
$first_character = $data[0];
83-
if ( '{' !== $first_character && '[' !== $first_character ) {
84+
$last_character = $data[-1];
85+
if ( ! ( '[' === $first_character && ']' === $last_character ) && ! ( '{' === $first_character && '}' === $last_character ) ) {
8486
return false;
8587
}
8688

@@ -246,9 +248,16 @@ protected function _import_phpspreadsheet( array $file ) /* : array|WP_Error */
246248

247249
// Apply data type formatting.
248250
$style = $spreadsheet->getCellXfByIndex( $cell->getXfIndex() );
251+
252+
$format = $style->getNumberFormat()->getFormatCode() ?? \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_GENERAL;
253+
254+
// Fix floating point precision issues with numbers in the "General" Excel .xlsx format.
255+
if ( 'xlsx' === $detected_format && \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_GENERAL === $format && is_numeric( $cell_data ) ) {
256+
$cell_data = (string) (float) $cell_data; // Type-cast strings to float and back.
257+
}
249258
$cell_data = \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::toFormattedString(
250259
$cell_data,
251-
$style->getNumberFormat() ? $style->getNumberFormat()->getFormatCode() : \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_GENERAL, // @phpstan-ignore-line
260+
$format,
252261
array( $this, 'format_color' )
253262
);
254263

Diff for: classes/class-import.php

+19-9
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,16 @@ protected function _get_import_files() /* : array|WP_Error */ {
127127
return new WP_Error( 'table_import_url_host_invalid', '', $this->import_config['url'] );
128128
}
129129

130-
// Check the host of the Import URL against a blacklist of hosts, which should not be accessible, e.g. for security considerations.
131-
$blocked_hosts = array(
132-
'169.254.169.254', // AWS Meta-data API.
130+
// Check the IP address of the host against a blocklist of hosts which should not be accessible, e.g. for security considerations.
131+
$ip = gethostbyname( $host ); // If no IP address can be found, this will return the host name, which will then be checked against the blocklist.
132+
$blocked_ips = array(
133+
'169.254.169.254', // Meta-data API for various cloud providers.
134+
'169.254.170.2', // AWS task metadata endpoint.
135+
'192.0.0.192', // Oracle Cloud endpoint.
136+
'100.100.100.200', // Alibaba Cloud endpoint.
133137
);
134-
if ( in_array( $host, $blocked_hosts, true ) ) {
135-
return new WP_Error( 'table_import_url_host_blocked', '', $this->import_config['url'] );
138+
if ( in_array( $ip, $blocked_ips, true ) ) {
139+
return new WP_Error( 'table_import_url_host_blocked', '', array( 'url' => $this->import_config['url'], 'ip' => $ip ) );
136140
}
137141

138142
/**
@@ -395,7 +399,7 @@ protected function _should_use_legacy_import_class(): bool {
395399
&& class_exists( 'ZipArchive', false )
396400
&& class_exists( 'DOMDocument', false )
397401
&& function_exists( 'simplexml_load_string' )
398-
&& function_exists( 'libxml_disable_entity_loader' );
402+
&& ( function_exists( 'libxml_disable_entity_loader' ) || PHP_VERSION_ID >= 80000 ); // This function is only needed for older versions of PHP.
399403
if ( ! $phpspreadsheet_requirements_fulfilled ) {
400404
return true;
401405
}
@@ -531,11 +535,17 @@ protected function _load_table_from_file_legacy( array $file ) /* : array|WP_Err
531535

532536
// If no format could be determined from the file extension, try guessing from the file content.
533537
if ( '' === $format ) {
538+
$data = trim( $data );
534539
$first_character = $data[0];
535-
if ( '<' === $first_character ) {
540+
$last_character = $data[-1];
541+
542+
if ( '<' === $first_character && '>' === $last_character ) {
536543
$format = 'html';
537-
} elseif ( '{' === $first_character || '[' === $first_character ) {
538-
$format = 'json';
544+
} elseif ( ( '[' === $first_character && ']' === $last_character ) || ( '{' === $first_character && '}' === $last_character ) ) {
545+
$json_table = json_decode( $data, true );
546+
if ( ! is_null( $json_table ) ) {
547+
$format = 'json';
548+
}
539549
}
540550
}
541551

Diff for: classes/class-tablepress.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ abstract class TablePress {
2727
* @since 1.0.0
2828
* @const string
2929
*/
30-
public const version = '2.2.4'; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
30+
public const version = '2.2.5'; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
3131

3232
/**
3333
* TablePress internal plugin version ("options scheme" version).
@@ -37,7 +37,7 @@ abstract class TablePress {
3737
* @since 1.0.0
3838
* @const int
3939
*/
40-
public const db_version = 70; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
40+
public const db_version = 73; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
4141

4242
/**
4343
* TablePress "table scheme" (data format structure) version.

Diff for: classes/class-view.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ protected function print_nav_tab_menu(): void {
411411
?>
412412
<div id="tablepress-header" class="header">
413413
<h1 class="name"><img src="<?php echo plugins_url( 'admin/img/tablepress-icon.png', TABLEPRESS__FILE__ ); ?>" class="tablepress-icon" alt="<?php esc_attr_e( 'TablePress plugin logo', 'tablepress' ); ?>" /><?php _e( 'TablePress', 'tablepress' ); ?></h1>
414-
<?php if ( tb_tp_fs()->is_free_plan() ) : ?>
414+
<?php if ( ! TABLEPRESS_IS_PLAYGROUND_PREVIEW && tb_tp_fs()->is_free_plan() ) : ?>
415415
<div class="buttons">
416416
<a href="https://tablepress.org/premium/?utm_source=plugin&utm_medium=button&utm_content=upgrade-button" class="tablepress-button">
417417
<span><?php _e( 'Upgrade to Premium', 'tablepress' ); ?></span>

Diff for: controllers/controller-admin.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ public function add_plugin_row_meta( array $links, string $file ): array {
410410
$links[] = '<a href="https://tablepress.org/faq/" title="' . esc_attr__( 'Frequently Asked Questions', 'tablepress' ) . '">' . __( 'FAQ', 'tablepress' ) . '</a>';
411411
$links[] = '<a href="https://tablepress.org/documentation/">' . __( 'Documentation', 'tablepress' ) . '</a>';
412412
$links[] = '<a href="https://tablepress.org/support/">' . __( 'Support', 'tablepress' ) . '</a>';
413-
if ( tb_tp_fs()->is_free_plan() ) {
413+
if ( ! TABLEPRESS_IS_PLAYGROUND_PREVIEW && tb_tp_fs()->is_free_plan() ) {
414414
$links[] = '<a href="https://tablepress.org/premium/?utm_source=plugin&utm_medium=textlink&utm_content=plugins-screen" title="' . esc_attr__( 'Check out the Premium version of TablePress!', 'tablepress' ) . '"><strong>' . __( 'Go Premium', 'tablepress' ) . '</strong></a>';
415415
}
416416
}
@@ -790,7 +790,7 @@ public function handle_post_action_add(): void {
790790

791791
$add_table = wp_unslash( $_POST['table'] );
792792

793-
// Perform sanity checks of posted data.
793+
// Perform confidence checks of posted data.
794794
$name = ( isset( $add_table['name'] ) ) ? $add_table['name'] : '';
795795
$description = ( isset( $add_table['description'] ) ) ? $add_table['description'] : '';
796796
if ( ! isset( $add_table['rows'], $add_table['columns'] ) ) {

Diff for: controllers/controller-admin_ajax.php

+2
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ public function ajax_action_save_table(): void {
174174
$response['table_id'] = $table['id']; // @phpstan-ignore-line
175175
$response['new_edit_nonce'] = wp_create_nonce( TablePress::nonce( 'edit', $table['id'] ) ); // @phpstan-ignore-line
176176
$response['new_preview_nonce'] = wp_create_nonce( TablePress::nonce( 'preview_table', $table['id'] ) ); // @phpstan-ignore-line
177+
$response['new_copy_nonce'] = wp_create_nonce( TablePress::nonce( 'copy_table', $table['id'] ) ); // @phpstan-ignore-line
178+
$response['new_delete_nonce'] = wp_create_nonce( TablePress::nonce( 'delete_table', $table['id'] ) ); // @phpstan-ignore-line
177179
$response['last_modified'] = TablePress::format_datetime( $table['last_modified'] ); // @phpstan-ignore-line
178180
$response['last_editor'] = TablePress::get_user_display_name( $table['options']['last_editor'] ); // @phpstan-ignore-line
179181
}

Diff for: controllers/controller-frontend.php

+8-6
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,8 @@ public function add_datatables_calls(): void {
253253
$commands = array();
254254

255255
foreach ( $this->shown_tables as $table_id => $table_store ) {
256+
$table_id = (string) $table_id; // Ensure that the table ID is a string, as it comes from an array key where numeric strings are converted to integers.
257+
256258
if ( empty( $table_store['instances'] ) ) {
257259
continue;
258260
}
@@ -514,7 +516,7 @@ public function shortcode_table( /* array|string */ $shortcode_atts ): string {
514516
// Check, if a table with the given ID exists.
515517
$table_id = (string) preg_replace( '/[^a-zA-Z0-9_-]/', '', $shortcode_atts['id'] );
516518
if ( ! TablePress::$model_table->table_exists( $table_id ) ) {
517-
$message = "[table &#8220;{$table_id}&#8221; not found /]<br />\n";
519+
$message = "&#91;table “{$table_id} not found /&#93;<br />\n";
518520
/**
519521
* Filters the "Table not found" message.
520522
*
@@ -530,7 +532,7 @@ public function shortcode_table( /* array|string */ $shortcode_atts ): string {
530532
// Load table, with table data, options, and visibility settings.
531533
$table = TablePress::$model_table->load( $table_id, true, true );
532534
if ( is_wp_error( $table ) ) {
533-
$message = "[table &#8220;{$table_id}&#8221; could not be loaded /]<br />\n";
535+
$message = "&#91;table “{$table_id} could not be loaded /&#93;<br />\n";
534536
/**
535537
* Filters the "Table could not be loaded" message.
536538
*
@@ -544,7 +546,7 @@ public function shortcode_table( /* array|string */ $shortcode_atts ): string {
544546
return $message;
545547
}
546548
if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
547-
$message = "<div>Attention: The internal data of table &#8220;{$table_id}&#8221; is corrupted!</div>";
549+
$message = "<div>Attention: The internal data of table {$table_id} is corrupted!</div>";
548550
/**
549551
* Filters the "Table data is corrupted" message.
550552
*
@@ -786,7 +788,7 @@ public function shortcode_table_info( /* array|string */ $shortcode_atts ): stri
786788
// Check, if a table with the given ID exists.
787789
$table_id = preg_replace( '/[^a-zA-Z0-9_-]/', '', $shortcode_atts['id'] );
788790
if ( ! TablePress::$model_table->table_exists( $table_id ) ) {
789-
$message = "[table &#8220;{$table_id}&#8221; not found /]<br />\n";
791+
$message = "&#91;table “{$table_id} not found /&#93;<br />\n";
790792
/** This filter is documented in controllers/controller-frontend.php */
791793
$message = apply_filters( 'tablepress_table_not_found_message', $message, $table_id );
792794
return $message;
@@ -795,7 +797,7 @@ public function shortcode_table_info( /* array|string */ $shortcode_atts ): stri
795797
// Load table, with table data, options, and visibility settings.
796798
$table = TablePress::$model_table->load( $table_id, true, true );
797799
if ( is_wp_error( $table ) ) {
798-
$message = "[table &#8220;{$table_id}&#8221; could not be loaded /]<br />\n";
800+
$message = "&#91;table “{$table_id} could not be loaded /&#93;<br />\n";
799801
/** This filter is documented in controllers/controller-frontend.php */
800802
$message = apply_filters( 'tablepress_table_load_error_message', $message, $table_id, $table );
801803
return $message;
@@ -860,7 +862,7 @@ public function shortcode_table_info( /* array|string */ $shortcode_atts ): stri
860862
$output = count( $table['data'][0] );
861863
break;
862864
default:
863-
$output = "[table-info field &#8220;{$field}&#8221; not found in table &#8220;{$table_id}&#8221; /]<br />\n";
865+
$output = "&#91;table-info field {$field} not found in table {$table_id}” /&#93;<br />\n";
864866
/**
865867
* Filters the "table info field not found" message.
866868
*

Diff for: libraries/freemius/includes/class-freemius.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -1357,8 +1357,7 @@ function _plugins_loaded() {
13571357
}
13581358

13591359
function _run_garbage_collector() {
1360-
// @todo - Remove this check once the garbage collector is ready to be out of beta.
1361-
if ( true !== fs_get_optional_constant( 'WP_FS__ENABLE_GARBAGE_COLLECTOR', false ) ) {
1360+
if ( true !== fs_get_optional_constant( 'WP_FS__ENABLE_GARBAGE_COLLECTOR', true ) ) {
13621361
return;
13631362
}
13641363

Diff for: libraries/freemius/includes/class-fs-garbage-collector.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ function clean() {
281281
$has_updated_option = false;
282282

283283
foreach ( $users as $user_id => $user ) {
284-
if ( ! isset( $user_has_install[ $user_id ] ) ) {
284+
if ( ! isset( $user_has_install_map[ $user_id ] ) ) {
285285
unset( $users[ $user_id ] );
286286

287287
foreach( $products_user_id_license_ids_map as $product_id => $user_id_license_ids_map ) {

Diff for: libraries/freemius/start.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*
1616
* @var string
1717
*/
18-
$this_sdk_version = '2.6.1';
18+
$this_sdk_version = '2.6.2';
1919

2020
#region SDK Selection Logic --------------------------------------------------------------------
2121

0 commit comments

Comments
 (0)