%s %s
' . esc_html( Plugin::dependency_message() ); if ( current_user_can( 'activate_plugins' ) ) { echo ' ' . esc_html__( 'Go to Plugins', 'tablerefresh' ) . ''; } echo '
' . esc_html__( 'Keep TablePress tables up to date with a CSV file or a public Google Sheet. TableRefresh replaces the table data with the data from the source; the table name, description, options, and shortcode stay the same.', 'tablerefresh' ) . '
'; if ( empty( $configs ) ) { echo '| ' . esc_html__( 'Table', 'tablerefresh' ) . ' | '; echo '' . esc_html__( 'Source', 'tablerefresh' ) . ' | '; echo '' . esc_html__( 'Automatic sync', 'tablerefresh' ) . ' | '; echo '' . esc_html__( 'Last sync', 'tablerefresh' ) . ' | '; echo '' . esc_html__( 'Actions', 'tablerefresh' ) . ' | '; echo '
|---|---|---|---|---|
| ' . esc_html( $name ) . '';
/* translators: %s: table ID. */
echo ' ' . esc_html( sprintf( __( 'Table ID %s', 'tablerefresh' ), $config['table_id'] ) ) . ' | ';
echo '' . esc_html( Sync_Config::source_label( $config['url'] ) ) . ' | '; echo '' . self::schedule_summary( $post_id, $config ) . ' | '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in schedule_summary(). echo '' . self::last_sync_summary( $post_id ) . ' | '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in last_sync_summary(). echo ''; if ( Sync_Config::ATTENTION_CONFLICT === $config['attention'] ) { // "Sync now" would stop at the edits again; the decision is on the settings screen. echo '' . esc_html__( 'Review changes', 'tablerefresh' ) . ''; } else { self::sync_button( $post_id, 'list' ); } echo ' ' . esc_html__( 'Settings and history', 'tablerefresh' ) . ' | '; echo '
← ' . esc_html__( 'All tables', 'tablerefresh' ) . '
'; self::print_notice(); if ( ! empty( $form['error'] ) ) { echo '' . esc_html( (string) $form['error'] ) . '
' . esc_html( empty( $names ) ? __( 'There are no TablePress tables yet. Create a table in TablePress first; TableRefresh fills existing tables.', 'tablerefresh' ) : __( 'All your TablePress tables are already set up for automatic sync. To sync another table, create it in TablePress first.', 'tablerefresh' ) ) . '
'; echo '' . esc_html__( 'Add a table in TablePress', 'tablerefresh' ) . '
' . self::schedule_summary( $post_id, (array) $config ) . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in schedule_summary(). if ( Sync_Config::ATTENTION_CONFLICT !== $config['attention'] ) { echo ''; self::sync_button( $post_id, 'edit' ); echo '
'; } self::render_history( $post_id ); echo '' . esc_html__( 'Deletes the sync settings and the sync history of this table and stops automatic sync. The TablePress table and its data are not changed.', 'tablerefresh' ) . '
'; echo ''; } /** * Explains why automatic sync is paused and offers the decisions. * * @param int $post_id Table post ID. * @param array' . esc_html__( 'Someone changed the table data in TablePress after the last sync. To keep those changes, TableRefresh did not update the table and paused automatic sync. Choose what to do:', 'tablerefresh' ) . '
'; echo '' . esc_html__( 'The table stays as it is and automatic sync is turned off.', 'tablerefresh' ) . '
'; self::action_form( 'keep', $post_id, __( 'Keep the changes', 'tablerefresh' ), 'button' ); echo '' . esc_html__( 'The changes made in TablePress are replaced with the current data from the source, and automatic sync continues.', 'tablerefresh' ) . '
'; echo ''; echo '' . esc_html( sprintf( __( 'The last %d syncs failed, so automatic sync was paused. The table was not changed. Check the details below and the data source, then click "Sync now". A successful sync turns automatic sync back on.', 'tablerefresh' ), (int) $config['failures'] ) ) . '
'; } echo '' . esc_html( sprintf( __( 'Last successful sync: %s', 'tablerefresh' ), self::format_time( $log['last_success'] ) ) ) . '
'; } if ( empty( $log['entries'] ) ) { echo '' . esc_html__( 'No syncs yet.', 'tablerefresh' ) . '
'; return; } $triggers = array( 'manual' => __( 'Sync now', 'tablerefresh' ), 'scheduled' => __( 'Automatic', 'tablerefresh' ), 'setup' => __( 'Setup', 'tablerefresh' ), 'replace' => __( 'Replaced changes', 'tablerefresh' ), ); echo '| ' . esc_html__( 'When', 'tablerefresh' ) . ' | ' . esc_html__( 'Started by', 'tablerefresh' ) . ' | ' . esc_html__( 'Result', 'tablerefresh' ) . ' | ' . esc_html__( 'Table data changed', 'tablerefresh' ) . ' | ' . esc_html__( 'Details', 'tablerefresh' ) . ' | '; echo '
|---|---|---|---|---|
| ' . esc_html( self::format_time( (int) $entry['time'] ) ) . ' | '; echo '' . esc_html( isset( $triggers[ $entry['trigger'] ] ) ? $triggers[ $entry['trigger'] ] : $entry['trigger'] ) . ' | '; echo '' . self::outcome_badge( (string) $entry['outcome'] ) . ' | '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in outcome_badge(). echo '' . esc_html( 'updated' === $entry['outcome'] ? __( 'Yes', 'tablerefresh' ) : __( 'No', 'tablerefresh' ) ) . ' | '; echo '' . esc_html( (string) $entry['message'] ) . ' | '; echo '
'; if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) { esc_html_e( 'This site runs WordPress scheduled tasks through a server cron job (DISABLE_WP_CRON). Automatic syncs run when that job calls wp-cron.php.', 'tablerefresh' ); } else { esc_html_e( 'Automatic syncs use WordPress scheduled tasks, which run when someone visits the site. On sites with few visitors, a sync can run later than planned.', 'tablerefresh' ); } echo '
'; } /** * Date and time in the site's format and time zone. * * @param int $time Unix time. * @return string */ private static function format_time( int $time ): string { return (string) wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $time ); } /* * --------------------------------------------------------------------------------------- * Form handlers (admin-post.php). * --------------------------------------------------------------------------------------- */ /** * Saves the settings of a table; the first setup also runs the first sync. */ public static function handle_save(): void { $post_id = isset( $_POST['table'] ) ? absint( $_POST['table'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified next. check_admin_referer( 'tablerefresh_save_' . $post_id ); $is_new = 0 === $post_id; $table_id = $is_new ? ( isset( $_POST['table_id'] ) ? sanitize_text_field( wp_unslash( $_POST['table_id'] ) ) : '' ) : TablePress_Adapter::get_table_id( $post_id ); $values = array( 'table_id' => $table_id, 'url' => isset( $_POST['url'] ) ? trim( (string) wp_unslash( $_POST['url'] ) ) : '', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Validated by Sync_Config::validate_url(); sanitizers would alter valid URLs (e.g. %20). 'frequency' => isset( $_POST['frequency'] ) ? sanitize_key( wp_unslash( $_POST['frequency'] ) ) : '', 'enabled' => ! empty( $_POST['enabled'] ), ); $back = $is_new ? self::url( array( 'action' => 'add' ) ) : self::edit_url( $post_id ); self::require_tablepress(); if ( $is_new && ( '' === $table_id || ! TablePress_Adapter::table_exists( $table_id ) ) ) { self::redirect_with_error( $back, $values, __( 'Select a table.', 'tablerefresh' ) ); } self::require_permission( $table_id ); if ( $is_new ) { $post_id = TablePress_Adapter::get_post_id( $table_id ); if ( $post_id <= 0 ) { self::redirect_with_error( $back, $values, __( 'The table could not be found in TablePress.', 'tablerefresh' ) ); } if ( null !== Sync_Config::get( $post_id ) ) { self::redirect_with_error( $back, $values, __( 'This table is already set up for automatic sync.', 'tablerefresh' ) ); } } elseif ( null === Sync_Config::get( $post_id ) ) { self::redirect_with_error( self::url(), array(), __( 'This table is no longer set up for automatic sync.', 'tablerefresh' ) ); } $url = Sync_Config::validate_url( $values['url'] ); if ( is_wp_error( $url ) ) { self::redirect_with_error( $back, $values, $url->get_error_message() ); } if ( ! in_array( $values['frequency'], Sync_Config::FREQUENCIES, true ) ) { self::redirect_with_error( $back, $values, __( 'Choose how often the table is synchronized.', 'tablerefresh' ) ); } if ( ( $is_new || null === Sync_State::read( $post_id ) ) && empty( $_POST['confirm_replace'] ) ) { self::redirect_with_error( $back, $values, __( 'Confirm that the current content of the table may be replaced with the data from the source.', 'tablerefresh' ) ); } $old = Sync_Config::get( $post_id ); $config = array( 'url' => $url, 'frequency' => $values['frequency'], 'enabled' => $values['enabled'], // A new source gets a fresh start; edits in TablePress still need a decision. 'attention' => $old && Sync_Config::ATTENTION_CONFLICT === $old['attention'] ? $old['attention'] : ( $old && $old['url'] === $url ? $old['attention'] : '' ), 'failures' => $old && $old['url'] === $url ? $old['failures'] : 0, 'created' => $old ? $old['created'] : time(), ); Sync_Config::save( $post_id, $config ); Scheduler::apply( $post_id, Sync_Config::get( $post_id ), null !== $old && $old['frequency'] !== $config['frequency'] ); if ( $is_new ) { $result = ( new Sync_Runner() )->run( $post_id, 'setup' ); self::set_notice( $result['outcome'], self::result_message( $result, $table_id ) ); self::redirect( self::url() ); } self::set_notice( 'saved', __( 'Settings saved.', 'tablerefresh' ) ); self::redirect( $back ); } /** * "Sync now". */ public static function handle_sync(): void { list( $post_id, $table_id ) = self::verify_table_request( 'sync' ); $result = ( new Sync_Runner() )->run( $post_id, 'manual' ); self::set_notice( $result['outcome'], self::result_message( $result, $table_id ) ); $back = isset( $_POST['back'] ) && 'edit' === $_POST['back'] ? self::edit_url( $post_id ) : self::url(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified in verify_table_request(). self::redirect( $back ); } /** * Replaces edits made in TablePress with the source data, after explicit confirmation. */ public static function handle_replace(): void { list( $post_id, $table_id ) = self::verify_table_request( 'replace' ); $back = self::edit_url( $post_id ); if ( empty( $_POST['confirm_replace'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified in verify_table_request(). self::set_notice( 'failed', __( 'Nothing was changed. Confirm that the changes made in TablePress may be replaced.', 'tablerefresh' ) ); self::redirect( $back ); } $result = ( new Sync_Runner() )->run( $post_id, 'replace' ); self::set_notice( $result['outcome'], self::result_message( $result, $table_id ) ); self::redirect( $back ); } /** * Keeps edits made in TablePress and turns automatic sync off. */ public static function handle_keep(): void { list( $post_id ) = self::verify_table_request( 'keep' ); $config = Sync_Config::get( $post_id ); if ( null !== $config ) { $config['enabled'] = false; $config['attention'] = ''; Sync_Config::save( $post_id, $config ); Scheduler::apply( $post_id, Sync_Config::get( $post_id ) ); } self::set_notice( 'saved', __( 'The changes made in TablePress are kept, and automatic sync is turned off. "Sync now" will again stop at these changes until you choose to replace them.', 'tablerefresh' ) ); self::redirect( self::edit_url( $post_id ) ); } /** * Removes the sync settings and history of a table. The table is not changed. */ public static function handle_remove(): void { list( $post_id ) = self::verify_table_request( 'remove' ); if ( empty( $_POST['confirm_remove'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified in verify_table_request(). self::set_notice( 'failed', __( 'Nothing was removed. Confirm that the sync settings and history may be deleted.', 'tablerefresh' ) ); self::redirect( self::edit_url( $post_id ) ); } Sync_Config::delete( $post_id ); Scheduler::apply( $post_id, null ); self::set_notice( 'saved', __( 'The sync setup was removed. The TablePress table and its data were not changed.', 'tablerefresh' ) ); self::redirect( self::url() ); } /** * Verifies nonce and permission of a request for a configured table. * * @param string $action Action. * @return array{0: int, 1: string} Post ID and table ID. */ private static function verify_table_request( string $action ): array { $post_id = isset( $_POST['table'] ) ? absint( $_POST['table'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified next. check_admin_referer( 'tablerefresh_' . $action . '_' . $post_id ); self::require_tablepress(); $table_id = TablePress_Adapter::get_table_id( $post_id ); self::require_permission( $table_id ); if ( null === Sync_Config::get( $post_id ) ) { self::set_notice( 'failed', __( 'This table is no longer set up for automatic sync.', 'tablerefresh' ) ); self::redirect( self::url() ); } return array( $post_id, $table_id ); } /** * Stops unless TablePress is available. */ private static function require_tablepress(): void { $error = TablePress_Adapter::availability_error(); if ( $error ) { wp_die( esc_html( $error->get_error_message() ), '', array( 'response' => 503 ) ); } } /** * Stops unless the current user may manage the sync of the table. * * @param string $table_id Table ID. */ private static function require_permission( string $table_id ): void { if ( ! self::can_manage( $table_id ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to manage the automatic sync of this table.', 'tablerefresh' ), '', array( 'response' => 403 ) ); } } /** * Notice text for a sync result. * * @param array{outcome: string, message: string} $result Result from Sync_Runner::run(). * @param string $table_id Table ID. * @return string */ private static function result_message( array $result, string $table_id ): string { $names = TablePress_Adapter::list_tables(); $name = isset( $names[ $table_id ] ) && '' !== $names[ $table_id ] ? $names[ $table_id ] : $table_id; /* translators: 1: table name, 2: result. */ return sprintf( __( '%1$s: %2$s', 'tablerefresh' ), $name, $result['message'] ); } /** * Stores a one-time notice for the current user. * * @param string $type Outcome or "saved". * @param string $message Message. */ private static function set_notice( string $type, string $message ): void { set_transient( 'tablerefresh_notice_' . get_current_user_id(), array( 'type' => $type, 'message' => $message, ), 5 * MINUTE_IN_SECONDS ); } /** * Prints and removes the one-time notice of the current user. */ private static function print_notice(): void { $key = 'tablerefresh_notice_' . get_current_user_id(); $notice = get_transient( $key ); if ( ! is_array( $notice ) || empty( $notice['message'] ) ) { return; } delete_transient( $key ); $classes = array( 'updated' => 'notice-success', 'unchanged' => 'notice-success', 'saved' => 'notice-success', 'conflict' => 'notice-warning', 'busy' => 'notice-warning', ); $class = isset( $classes[ $notice['type'] ] ) ? $classes[ $notice['type'] ] : 'notice-error'; echo '' . esc_html( (string) $notice['message'] ) . '