] * : A .csv or .xlsx file to import. Leave it out to use the source set in * Settings (the uploaded file or the Google Sheet). The file is read in * place; it doesn't replace the upload stored on the site. * * [--dry-run] * : Show what would happen, including what would change in each post, without saving anything. * * [--rows=] * : Only these sheet rows, by the row number the spreadsheet shows, e.g. 2,5,9. They're imported even if the row filter would leave them out. * * [--all] * : Ignore the row filter and import every row. * * [--auto-map] * : Map the columns by their names, as for a fresh upload, instead of using the saved mapping. * * [--force] * : Re-import rows even when they haven't changed since the last import. * * [--format=] * : table, csv, json or yaml. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * ## EXAMPLES * * wp steadypost import --dry-run * wp steadypost import posts.xlsx * wp steadypost import pages.csv --auto-map --rows=2,3 * wp steadypost import --format=json * * @param array $args Positional arguments. * @param array $assoc_args Options. */ public function import( $args, $assoc_args ) { $file = isset( $args[0] ) ? (string) $args[0] : ''; $dry = ! empty( $assoc_args['dry-run'] ); self::act_as_default_author(); $prepared = self::prepare( $file, array( 'rows' => isset( $assoc_args['rows'] ) ? (string) $assoc_args['rows'] : '', 'all' => ! empty( $assoc_args['all'] ), 'auto_map' => ! empty( $assoc_args['auto-map'] ), 'force' => ! empty( $assoc_args['force'] ), ) ); if ( is_wp_error( $prepared ) ) { WP_CLI::error( $prepared->get_error_message() ); } foreach ( $prepared['notes'] as $note ) { WP_CLI::warning( wp_strip_all_tags( $note ) ); } if ( ! $prepared['rows'] ) { WP_CLI::warning( 'No rows to import.' ); return; } $progress = ( 'table' === ( $assoc_args['format'] ?? 'table' ) && count( $prepared['rows'] ) > 1 ) ? \WP_CLI\Utils\make_progress_bar( $dry ? 'Checking rows' : 'Importing', count( $prepared['rows'] ) ) : null; $run = self::run( $prepared, $dry, function () use ( $progress ) { if ( $progress ) { $progress->tick(); } } ); if ( $progress ) { $progress->finish(); } $items = array(); foreach ( $run['results'] as $result ) { $items[] = array( 'row' => $result['row'], 'status' => $result['status'], 'post_id' => $result['post_id'] ? $result['post_id'] : '', 'title' => $result['title'], 'message' => trim( $result['message'] . ' ' . implode( ' ', (array) $result['notes'] ) ), 'changes' => self::changes_text( $result ), ); } $fields = $dry ? array( 'row', 'status', 'post_id', 'title', 'message', 'changes' ) : array( 'row', 'status', 'post_id', 'title', 'message' ); WP_CLI\Utils\format_items( $assoc_args['format'] ?? 'table', $items, $fields ); if ( 'table' !== ( $assoc_args['format'] ?? 'table' ) ) { return; } $counts = self::counts( $run['results'] ); $line = $dry ? sprintf( 'Dry run: %d to create, %d to update, %d skipped, %d with errors. Nothing was saved.', $counts['create'], $counts['update'], $counts['skipped'], $counts['error'] ) : sprintf( 'Done: %d created, %d updated, %d skipped, %d failed. Undo with: wp steadypost undo %s', $counts['created'], $counts['updated'], $counts['skipped'], $counts['error'], $run['run_id'] ); if ( $counts['error'] ) { WP_CLI::error( $line, false ); WP_CLI::halt( 1 ); } WP_CLI::success( $line ); } /** * List recent imports. * * ## OPTIONS * * [--format=] * : table, csv, json or yaml. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * @param array $args Positional arguments. * @param array $assoc_args Options. */ public function history( $args, $assoc_args ) { $items = array(); foreach ( SteadyPost_History::all() as $run ) { $items[] = array( 'id' => $run['id'], 'started' => wp_date( 'Y-m-d H:i', $run['started'] ), 'trigger' => $run['trigger'], 'source' => $run['source'], 'created' => $run['counts']['created'], 'updated' => $run['counts']['updated'], 'skipped' => $run['counts']['skipped'], 'failed' => $run['counts']['error'], 'state' => SteadyPost_History::state( $run ), ); } WP_CLI\Utils\format_items( $assoc_args['format'] ?? 'table', $items, array( 'id', 'started', 'trigger', 'source', 'created', 'updated', 'skipped', 'failed', 'state' ) ); } /** * Undo an import: posts it created go to the Trash, posts it updated get back the version from before it. * * ## OPTIONS * * * : The run's ID, from `wp steadypost history` or the end of `wp steadypost import`. * * [--yes] * : Don't ask for confirmation. * * @param array $args Positional arguments. * @param array $assoc_args Options. */ public function undo( $args, $assoc_args ) { $run = SteadyPost_History::get( (string) $args[0] ); if ( ! $run ) { WP_CLI::error( 'No import with that ID is in the history.' ); } if ( ! empty( $run['undone'] ) ) { WP_CLI::error( 'That import has already been undone.' ); } self::act_as_default_author(); $plan = SteadyPost_History::undo_plan( $run['id'] ); WP_CLI::confirm( sprintf( 'Move %d post(s) to the Trash and restore %d to their earlier version?', count( $plan['trash'] ), count( $plan['restore'] ) ), $assoc_args ); $done = SteadyPost_History::undo( $run['id'] ); if ( $done['cannot'] ) { WP_CLI::warning( sprintf( '%d updated post(s) had no earlier revision to go back to.', $done['cannot'] ) ); } WP_CLI::success( sprintf( 'Import undone: %d post(s) moved to the Trash, %d restored.', $done['trashed'], $done['restored'] ) ); } /* --------------------------------------------------------------- the work */ /** * Read the rows an import would run on. * * @param string $file Path to a .csv/.xlsx, or '' for the configured source. * @param array $options 'rows' (comma list), 'all', 'auto_map', 'force'. * @return array|WP_Error 'rows', 'settings', 'notes', 'source' */ public static function prepare( $file, $options ) { $settings = SteadyPost_Settings::all(); $sheet = '' !== $file ? SteadyPost_Sheet::fetch_file( $file, $settings ) : SteadyPost_Sheet::fetch( $settings ); if ( is_wp_error( $sheet ) ) { return $sheet; } // A file the saved mapping doesn't fit gets mapped by its column // names, as a fresh upload would. $title_col = isset( $settings['mapping']['title'] ) ? $settings['mapping']['title'] : ''; if ( ! empty( $options['auto_map'] ) || ( '' !== $file && ( '' === $title_col || ! in_array( $title_col, $sheet['headers'], true ) ) ) ) { $settings['mapping'] = SteadyPost_Sheet::auto_map( $sheet['headers'] ); } if ( ! SteadyPost_Settings::has_title( $settings ) ) { return new WP_Error( 'steadypost_no_title', __( 'No column is mapped to the post title and no title template is set. Set one in Settings, or use --auto-map.', 'steadypost' ) ); } $settings['skip_unchanged'] = empty( $options['force'] ) && ! empty( $settings['sync_skip_unchanged'] ); $notes = $sheet['notes']; $wanted = array(); foreach ( explode( ',', (string) ( $options['rows'] ?? '' ) ) as $number ) { if ( absint( trim( $number ) ) > 0 ) { $wanted[] = absint( trim( $number ) ); } } $rows = array(); foreach ( $sheet['rows'] as $row ) { $number = (int) $row[ SteadyPost_Sheet::ROW_KEY ]; if ( $wanted ) { if ( in_array( $number, $wanted, true ) ) { $rows[] = $row; } continue; } $gate = SteadyPost_Importer::gate_check( $row, $settings ); if ( ! empty( $options['all'] ) ? ! $gate['blocked'] : $gate['ok'] ) { $rows[] = $row; } } if ( ! $wanted && count( $rows ) < count( $sheet['rows'] ) ) { /* translators: %d: number of rows */ $notes[] = sprintf( _n( '%d row was left out by the row filter or has no title.', '%d rows were left out by the row filter or have no title.', count( $sheet['rows'] ) - count( $rows ), 'steadypost' ), count( $sheet['rows'] ) - count( $rows ) ); } return array( 'rows' => $rows, 'settings' => $settings, 'notes' => $notes, 'source' => '' !== $file ? wp_basename( $file ) : SteadyPost_Admin::source_label( $settings ), ); } /** * Import (or dry-run) prepared rows. A real run is recorded in History. * * @param array $prepared From prepare(). * @param bool $dry Dry run. * @param callable|null $tick Called after each row. * @return array 'run_id' ('' for a dry run), 'results' */ public static function run( $prepared, $dry, $tick = null ) { $run_id = $dry ? '' : wp_generate_uuid4(); if ( ! $dry ) { SteadyPost_History::start( $run_id, array( 'source' => $prepared['source'], 'total' => count( $prepared['rows'] ), 'trigger' => 'cli', ) ); } $results = array(); $batch = array(); foreach ( $prepared['rows'] as $row ) { $result = $dry ? SteadyPost_Importer::plan_row( $row, $prepared['settings'] ) : SteadyPost_Importer::import_row( $row, $prepared['settings'] ); $results[] = $result; $batch[] = $result; // Written as it goes, so an interrupted run still shows in History // with the rows it got through, and can be undone. if ( ! $dry && count( $batch ) >= 10 ) { SteadyPost_History::add( $run_id, $batch, false ); $batch = array(); } if ( $tick ) { call_user_func( $tick ); } } if ( ! $dry ) { SteadyPost_History::add( $run_id, $batch, true ); } return array( 'run_id' => $run_id, 'results' => $results, ); } /** Status => count, for both real (created/updated) and dry (create/update) runs. */ public static function counts( $results ) { $counts = array_fill_keys( array( 'created', 'updated', 'create', 'update', 'skipped', 'error' ), 0 ); foreach ( $results as $result ) { $status = isset( $counts[ $result['status'] ] ) ? $result['status'] : 'error'; ++$counts[ $status ]; } return $counts; } /** A dry run's field changes as one line: "Title: old → new; Status: draft → publish". */ public static function changes_text( $result ) { $parts = array(); foreach ( isset( $result['changes'] ) ? $result['changes'] : array() as $change ) { $parts[] = $change['field'] . ': ' . ( '' === $change['old'] ? '(empty)' : $change['old'] ) . ' → ' . ( '' === $change['new'] ? '(empty)' : $change['new'] ); } return implode( '; ', $parts ); } /** * WP-CLI runs as nobody unless --user is given, but the importer checks * capabilities (edit_post on a targeted post, unfiltered_html for content). * Act as the default author, as a scheduled sync does. */ protected static function act_as_default_author() { if ( get_current_user_id() ) { return; } $author = (int) SteadyPost_Settings::get( 'default_author' ); if ( ! $author ) { // Settings never saved: the first administrator. $admins = get_users( array( 'role' => 'administrator', 'number' => 1, 'orderby' => 'ID', 'fields' => 'ID', ) ); $author = $admins ? (int) $admins[0] : 0; } wp_set_current_user( $author ); } }