admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( self::NONCE ), 'reportUrl' => self::url( array( 'step' => 'report' ) ), 'i18n' => array( 'startError' => __( 'The import could not start. Reload the page and try again.', 'prodtabs' ), 'error' => __( 'The import stopped. Reload the page: it will resume where it stopped.', 'prodtabs' ), 'done' => __( 'Import finished.', 'prodtabs' ), 'none' => __( 'Select at least one product, or choose to import all the products.', 'prodtabs' ), ), ) ); } /* Screen --------------------------------------------------------------------------------------------------- */ /** Screen content. */ public static function render(): void { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to import tabs.', 'prodtabs' ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- navigation between read-only steps. $step = isset( $_GET['step'] ) ? sanitize_key( wp_unslash( $_GET['step'] ) ) : 'analysis'; $state = Import_Yikes::state(); if ( 'running' === ( $state['status'] ?? '' ) && 'run' !== $step ) { $step = 'resume'; } $steps = array( 'analysis' => __( 'Analysis', 'prodtabs' ), 'preview' => __( 'Preview', 'prodtabs' ), 'run' => _x( 'Import', 'noun', 'prodtabs' ), 'report' => __( 'Report', 'prodtabs' ), ); $current = 'resume' === $step ? 'run' : $step; ?>

    $label ) : ?> >
' . esc_html__( 'No tabs of Custom Product Tabs (YIKES) were found on this site: there is nothing to import.', 'prodtabs' ) . '

'; echo '

' . esc_html__( 'Go to the shared tabs', 'prodtabs' ) . '

'; return; } $warnings = self::warnings( $a ); ?>

' . esc_html( implode( ' ', $lines ) ) . '

'; } /** * Warnings of the analysis, as sentences. * * @param array $a Analysis. * @return string[] */ private static function warnings( array $a ): array { $out = array(); if ( $a['has_tabs'] ) { /* translators: %d: number of products. */ $out[] = sprintf( _n( '%d product already has ProdTabs tabs made by hand: it is skipped, with its translations, unless you choose to import it again (its tabs are then kept aside and put back if you undo the import).', '%d products already have ProdTabs tabs made by hand: they are skipped, with their translations, unless you choose to import them again (their tabs are then kept aside and put back if you undo the import).', $a['has_tabs'], 'prodtabs' ), $a['has_tabs'] ); } if ( $a['duplicate'] ) { /* translators: %d: number of tabs. */ $out[] = sprintf( _n( '%d tab was never shown by YIKES because another tab of the product has the same title: it is imported hidden, so that you can check it and show it.', '%d tabs were never shown by YIKES because another tab of the product has the same title: they are imported hidden, so that you can check them and show them.', $a['duplicate'], 'prodtabs' ), $a['duplicate'] ); } if ( $a['untitled'] ) { /* translators: %d: number of tabs. */ $out[] = sprintf( _n( '%d tab has no title, so YIKES never showed it: it is imported hidden.', '%d tabs have no title, so YIKES never showed them: they are imported hidden.', $a['untitled'], 'prodtabs' ), $a['untitled'] ); } if ( $a['replaces_native'] ) { /* translators: %d: number of tabs. */ $out[] = sprintf( _n( '%d tab has the title of a WooCommerce tab (Description, Reviews) and replaced it: it is imported, and the WooCommerce tab is hidden on that product.', '%d tabs have the title of a WooCommerce tab (Description, Reviews) and replaced it: they are imported, and the WooCommerce tab is hidden on those products.', $a['replaces_native'], 'prodtabs' ), $a['replaces_native'] ); } if ( $a['differs'] ) { /* translators: %d: number of tabs. */ $out[] = sprintf( _n( '%d tab is linked to a saved tab but its text differs: it is imported as a customized copy of the shared tab.', '%d tabs are linked to a saved tab but their text differs: they are imported as customized copies of the shared tab.', $a['differs'], 'prodtabs' ), $a['differs'] ); } if ( $a['looks_saved'] ) { /* translators: %d: number of tabs. */ $out[] = sprintf( _n( '%d tab has the same text as a saved tab without being linked to it: it is imported as a customized copy, with a button to go back to the shared version.', '%d tabs have the same text as a saved tab without being linked to it: they are imported as customized copies, with a button to go back to the shared version.', $a['looks_saved'], 'prodtabs' ), $a['looks_saved'] ); } if ( $a['unreadable'] ) { /* translators: %s: product ids. */ $out[] = sprintf( __( 'Tabs that cannot be read (damaged data), products %s: they are left out.', 'prodtabs' ), implode( ', ', array_slice( $a['unreadable'], 0, 20 ) ) ); } if ( $a['repaired'] ) { /* translators: %d: number of products. */ $out[] = sprintf( _n( '%d product has tabs edited outside WordPress (spreadsheet): they were repaired for the import, check them.', '%d products have tabs edited outside WordPress (spreadsheet): they were repaired for the import, check them.', count( $a['repaired'] ), 'prodtabs' ), count( $a['repaired'] ) ); } if ( $a['shortcodes'] ) { /* translators: %s: shortcode names. */ $out[] = sprintf( __( 'Shortcodes of plugins that are not active: %s. They will show as plain text until their plugin is active.', 'prodtabs' ), '[' . implode( '], [', array_keys( $a['shortcodes'] ) ) . ']' ); } if ( $a['groups'] && $a['groups'] < $a['products'] ) { /* translators: %d: number of groups. */ $out[] = sprintf( _n( 'Products and their translations are imported together (%d group): one list of tabs for all their languages, with the texts of each language matched by position.', 'Products and their translations are imported together (%d groups): one list of tabs for all their languages, with the texts of each language matched by position.', $a['groups'], 'prodtabs' ), $a['groups'] ); } if ( $a['missing_translation'] ) { /* translators: %d: number of tabs. */ $out[] = sprintf( _n( '%d tab of a translation had no equivalent with YIKES: that language shows the text of the default language until you translate it.', '%d tabs of translations had no equivalent with YIKES: those languages show the text of the default language until you translate them.', $a['missing_translation'], 'prodtabs' ), $a['missing_translation'] ); } if ( $a['not_in_source'] ) { /* translators: %d: number of tabs. */ $out[] = sprintf( _n( '%d tab of a translation does not exist in the default language: it is added to the list and shown only in the languages that have it.', '%d tabs of translations do not exist in the default language: they are added to the list and shown only in the languages that have them.', $a['not_in_source'], 'prodtabs' ), $a['not_in_source'] ); } if ( $a['pro'] ) { /* translators: %d: number of saved tabs. */ $out[] = sprintf( _n( '%d saved tab was a global or category tab of YIKES Pro: it is imported as a shared tab, to be added to products.', '%d saved tabs were global or category tabs of YIKES Pro: they are imported as shared tabs, to be added to products.', $a['pro'], 'prodtabs' ), $a['pro'] ); } if ( $a['imported'] ) { /* translators: %d: number of products. */ $out[] = sprintf( _n( '%d product was already imported: it is skipped, unless you choose to import it again.', '%d products were already imported: they are skipped, unless you choose to import them again.', $a['imported'], 'prodtabs' ), $a['imported'] ); } return $out; } /** * "3 products", "1 saved tab"… * * @param string $what products or saved. * @param int $count Number. */ private static function count_label( string $what, int $count ): string { switch ( $what ) { case 'products': /* translators: %d: number of products. */ return sprintf( _n( '%d product', '%d products', $count, 'prodtabs' ), $count ); default: /* translators: %d: number of saved tabs. */ return sprintf( _n( '%d saved tab', '%d saved tabs', $count, 'prodtabs' ), $count ); } } /** Step 2: products and the tabs they will get, options, start. */ private static function step_preview(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only pagination. $page = isset( $_GET['paged'] ) ? max( 1, absint( $_GET['paged'] ) ) : 1; $per_page = 20; $preview = Import_Yikes::preview( $page, $per_page ); $pages = (int) ceil( $preview['total'] / $per_page ); ?>




1 ) : ?>

add_query_arg( 'paged', '%#%', self::url( array( 'step' => 'preview' ) ) ), 'format' => '', 'current' => $page, 'total' => $pages, 'prev_text' => __( 'Previous', 'prodtabs' ), 'next_text' => __( 'Next', 'prodtabs' ), ) ) ); ?>

$tab ) { $titles[ $id ] = $tab['title']; } $natives = array( 'description' => __( 'Description', 'prodtabs' ), 'additional_information' => __( 'Additional information', 'prodtabs' ), 'reviews' => __( 'Reviews', 'prodtabs' ), ); $hidden = array(); echo '
    '; foreach ( $entries as $entry ) { if ( 'native' === $entry['type'] ) { if ( false === $entry['visible'] ) { $hidden[] = $natives[ $entry['key'] ]; } continue; } if ( 'local' === $entry['type'] && '' === $entry['title'] && '' === trim( $entry['content'] ) ) { $label = 'fallback' === ( $entry['note'] ?? '' ) ? __( '(not translated: shown in the default language)', 'prodtabs' ) : __( '(tab of another language: not shown in this one)', 'prodtabs' ); echo '
  1. ' . esc_html( $label ) . '
  2. '; continue; } $label = 'library' === $entry['type'] ? ( $titles[ $entry['library_id'] ] ?? '' ) : $entry['title']; echo '
  3. ' . esc_html( '' !== $label ? $label : __( '(no title)', 'prodtabs' ) ) . ' '; if ( 'library' === $entry['type'] ) { echo '' . esc_html__( 'Shared', 'prodtabs' ) . ''; } elseif ( $entry['origin'] ) { echo '' . esc_html__( 'Customized', 'prodtabs' ) . ''; } if ( ! $entry['visible'] ) { echo ' ' . esc_html__( 'hidden', 'prodtabs' ) . ''; } echo '
  4. '; } echo '
'; foreach ( $hidden as $label ) { /* translators: %s: name of a WooCommerce tab. */ echo '

' . esc_html( sprintf( __( 'WooCommerce tab ā€œ%sā€ hidden on this product.', 'prodtabs' ), $label ) ) . '

'; } } /** * Progress bar. * * @param int $done Products done. * @param int $total Products to do. */ private static function progress( int $done, int $total ): void { $percent = $total ? (int) floor( 100 * $done / $total ) : 0; ?>

'report' ) ) ); exit; } /** * Step 4: report. * * @param array $state Import state. */ private static function step_report( array $state ): void { if ( 'done' !== ( $state['status'] ?? '' ) ) { echo '

' . esc_html__( 'No import has been run yet.', 'prodtabs' ) . '

'; echo '

' . esc_html__( 'Start with the analysis', 'prodtabs' ) . '

'; return; } $c = $state['counts']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- message after a redirect. if ( isset( $_GET['deactivated'] ) ) { echo '

' . esc_html__( 'Custom Product Tabs (YIKES) is deactivated. Its data stays in the database: do not delete the plugin until you have checked your products, deleting it erases its data.', 'prodtabs' ) . '

'; } ?>

()

1, 'cleaned' => $undone['products'], 'restored' => $undone['restored'], 'kept' => $undone['kept'], ) ) ); exit; } /** * Tabs to look at after the import, by kind. * * @param array $notes Notes: product id, code, tab title. */ private static function report_notes( array $notes ): void { if ( ! $notes ) { return; } $labels = array( 'duplicate' => __( 'Imported hidden: same title as another tab of the product', 'prodtabs' ), 'untitled' => __( 'Imported hidden: no title', 'prodtabs' ), 'replaces_native' => __( 'Replaces a WooCommerce tab, which is hidden', 'prodtabs' ), 'differs' => __( 'Customized copy of a shared tab (its text differed)', 'prodtabs' ), 'looks_saved' => __( 'Customized copy of a shared tab (same text, not linked)', 'prodtabs' ), 'repaired' => __( 'Tabs repaired (edited outside WordPress)', 'prodtabs' ), 'not_in_source' => __( 'Tab missing from the default language: shown only in the languages that have it', 'prodtabs' ), 'missing_translation' => __( 'Not translated yet: shown in the default language', 'prodtabs' ), 'has_tabs' => __( 'Skipped: the product already had ProdTabs tabs', 'prodtabs' ), ); $by_code = array(); foreach ( $notes as $note ) { $by_code[ $note[1] ][] = $note; } echo '

' . esc_html__( 'Tabs to look at', 'prodtabs' ) . '

'; foreach ( $by_code as $code => $items ) { echo '

' . esc_html( $labels[ $code ] ?? $code ) . '

'; } } /* AJAX ----------------------------------------------------------------------------------------------------- */ /** Checks an AJAX request of the screen. */ private static function check(): void { check_ajax_referer( self::NONCE, 'nonce' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( null, 403 ); } } /** AJAX: starts the import. */ public static function ajax_start(): void { self::check(); // phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce checked by check() above. $scope = isset( $_POST['scope'] ) ? sanitize_key( wp_unslash( $_POST['scope'] ) ) : 'all'; $products = isset( $_POST['products'] ) && is_array( $_POST['products'] ) ? array_map( 'absint', wp_unslash( $_POST['products'] ) ) : array(); if ( 'selected' === $scope && ! $products ) { wp_send_json_error( null, 400 ); } $state = Import_Yikes::start( array( 'products' => 'selected' === $scope ? $products : array(), 'overwrite' => ! empty( $_POST['overwrite'] ), 'the_content' => isset( $_POST['the_content'] ) ? ! empty( $_POST['the_content'] ) : null, ) ); // phpcs:enable wp_send_json_success( self::summary( $state ) ); } /** AJAX: imports the next batch. */ public static function ajax_step(): void { self::check(); wp_send_json_success( self::summary( Import_Yikes::step() ) ); } /** * Progress sent to the script. * * @param array $state Import state. */ private static function summary( array $state ): array { $done = (int) ( $state['done'] ?? 0 ); $total = (int) ( $state['total'] ?? 0 ); return array( 'status' => $state['status'] ?? '', 'done' => $done, 'total' => $total, /* translators: 1: products done, 2: total number of products. */ 'text' => sprintf( _n( '%1$d of %2$d product', '%1$d of %2$d products', $total, 'prodtabs' ), $done, $total ), ); } /** Deactivates YIKES from the report. */ public static function deactivate_yikes(): void { check_admin_referer( 'prodtabs_deactivate_yikes' ); if ( ! current_user_can( 'deactivate_plugin', Import_Yikes::PLUGIN_FILE ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to deactivate plugins.', 'prodtabs' ) ); } // As on the Plugins screen of a site: a plugin active on the whole network is deactivated from the network. if ( self::yikes_network_active() ) { wp_die( esc_html__( 'Custom Product Tabs (YIKES) is active on the whole network: only the network admin can deactivate it, from the Plugins screen of the network, once the other sites no longer use it.', 'prodtabs' ) ); } deactivate_plugins( Import_Yikes::PLUGIN_FILE, false, false ); wp_safe_redirect( self::url( array( 'step' => 'report', 'deactivated' => 1 ) ) ); exit; } /** Whether YIKES is active on the whole network of sites. */ private static function yikes_network_active(): bool { return is_multisite() && is_plugin_active_for_network( Import_Yikes::PLUGIN_FILE ); } /* Notice --------------------------------------------------------------------------------------------------- */ /** Notice on the Plugins screen and the plugin's screens while YIKES data waits to be imported. */ public static function notice(): void { $screen = get_current_screen(); $ours = array( 'plugins', 'edit-' . Library::POST_TYPE, 'woocommerce_page_wc-settings' ); if ( ! $screen || ! in_array( $screen->id, $ours, true ) || self::is_screen() || ! current_user_can( 'manage_woocommerce' ) ) { return; } if ( 'woocommerce_page_wc-settings' === $screen->id && ! self::is_settings_section() ) { return; } if ( get_user_meta( get_current_user_id(), self::NOTICE, true ) || Import_Yikes::state() ) { return; } $found = Import_Yikes::detect(); if ( ! $found['products'] && ! $found['reusable'] ) { return; } $dismiss = wp_nonce_url( add_query_arg( 'prodtabs_dismiss', 'yikes' ), 'prodtabs_dismiss_yikes' ); ?>