/**
  * {@inheritdoc}
  * @param array $args
  * * dir - source directory in which is located `database.json.txt`
  * * [full] - (bool) force full or content restore. if not specified, will be detected automatically
  * * [required] - (default: false) if database file must exist, else if db restore is optional
  */
 public function execute(array $args, array $state = array())
 {
     if (!isset($args['dir'])) {
         return new WP_Error('no_source_dir', __('Source dir not specified', 'fw'));
     } else {
         $args['dir'] = fw_fix_path($args['dir']);
     }
     if (!isset($args['required'])) {
         $args['required'] = false;
     } else {
         $args['required'] = (bool) $args['required'];
     }
     if (empty($state)) {
         if (!file_exists($args['dir'] . '/database.json.txt')) {
             if ($args['required']) {
                 return new WP_Error('no_db_file', __('Database file not found', 'fw'));
             } else {
                 return true;
             }
         }
         $state = array('task' => 'cleanup', 'step' => 0, 'params' => array(), 'tables' => array(), 'full' => isset($args['full']) ? (bool) $args['full'] : null);
     }
     global $wpdb;
     /** @var WPDB $wpdb */
     if ($state['task'] === 'cleanup') {
         // delete all tables with temporary prefix $this->get_tmp_table_prefix()
         if ($table_names = $wpdb->get_col($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($this->get_tmp_table_prefix()) . '%'))) {
             if (!$wpdb->query('DROP TABLE ' . esc_sql($table_name = array_pop($table_names)))) {
                 return new WP_Error('drop_tmp_table_fail', sprintf(__('Cannot drop temporary table: %s', 'fw'), $table_name));
             }
             return $state;
         } else {
             $state['task'] = 'inspect';
             $state['step'] = 0;
             return $state;
         }
     } elseif ($state['task'] === 'inspect') {
         try {
             $fo = new SplFileObject($args['dir'] . '/database.json.txt');
         } catch (RuntimeException $e) {
             $fo = null;
             return new WP_Error('cannot_open_file', __('Cannot open db file', 'fw'));
         }
         try {
             $fo->seek($state['step']);
         } catch (RuntimeException $e) {
             $fo = null;
             return new WP_Error('cannot_move_file_cursor', __('Cannot move cursor in db file', 'fw'));
         }
         $started_time = time();
         $timeout = fw_ext('backups')->get_timeout() - 7;
         while (time() - $started_time < $timeout) {
             if ($line = $fo->current()) {
                 if (is_null($line = json_decode($line, true))) {
                     $fo = null;
                     return new WP_Error('line_decode_fail', sprintf(__('Failed to decode line %d from db file.', 'fw') . ' ' . fw_get_json_last_error_message(), $state['step'] + 1));
                 }
                 if ($line['type'] === 'row' && $line['data']['table'] === 'options' && isset($line['data']['row']['option_name']) && in_array($line['data']['row']['option_name'], array('siteurl', 'home'))) {
                     $state['params'][$line['data']['row']['option_name']] = $line['data']['row']['option_value'];
                 } elseif ($line['type'] === 'table' && !isset($state['tables'][$line['data']['name']])) {
                     $state['tables'][$line['data']['name']] = true;
                 } elseif ($line['type'] === 'param') {
                     $state['params'][$line['data']['name']] = $line['data']['value'];
                 }
             } elseif ($line === false && !$fo->eof()) {
                 $fo = null;
                 return new WP_Error('line_read_fail', sprintf(__('Cannot read line %d from db file', 'fw'), $state['step'] + 1));
             } else {
                 if (!isset($state['params']['siteurl']) || !isset($state['params']['home'])) {
                     return new WP_Error('params_not_found', __('Required params not found', 'fw'));
                 }
                 $is_full_backup = isset($state['tables']['commentmeta']) && isset($state['tables']['comments']) && isset($state['tables']['links']) && isset($state['tables']['options']) && isset($state['tables']['postmeta']) && isset($state['tables']['posts']) && isset($state['tables']['terms']) && isset($state['tables']['term_relationships']) && isset($state['tables']['term_taxonomy']) && isset($state['tables']['usermeta']) && isset($state['tables']['users']);
                 if (is_multisite()) {
                     /* @link https://codex.wordpress.org/Database_Description */
                     $is_full_backup = $is_full_backup && (isset($state['tables']['blogs']) && isset($state['tables']['blog_versions']) && isset($state['tables']['registration_log']) && isset($state['tables']['signups']) && isset($state['tables']['site']) && isset($state['tables']['sitemeta']));
                 }
                 if (is_null($state['full'])) {
                     $state['full'] = $is_full_backup;
                 } elseif ($state['full'] && !$is_full_backup) {
                     return new WP_Error('full_db_restore_impossible', __('Cannot do full db restore because backup is missing some tables', 'fw'));
                 }
                 $skip_tables = array('users' => true, 'usermeta' => true);
                 if (!$state['full']) {
                     $skip_tables = array_merge($skip_tables, array('blogs' => true, 'blog_versions' => true, 'registration_log' => true, 'signups' => true, 'site' => true, 'sitemeta' => true, 'sitecategories' => true));
                 }
                 foreach (array_keys($skip_tables) as $table_name) {
                     if (isset($state['tables'][$table_name])) {
                         $state['tables'][$table_name] = false;
                     }
                 }
                 unset($skip_tables);
                 $state['step'] = 0;
                 $state['task'] = 'import';
                 $fo = null;
                 return $state;
             }
             $state['step']++;
             $fo->next();
         }
         $fo = null;
     } elseif ($state['task'] === 'import') {
         try {
             $fo = new SplFileObject($args['dir'] . '/database.json.txt');
         } catch (RuntimeException $e) {
             $fo = null;
             return new WP_Error('cannot_open_file', __('Cannot open db file', 'fw'));
         }
         try {
             $fo->seek($state['step']);
         } catch (RuntimeException $e) {
             $fo = null;
             return new WP_Error('cannot_move_file_cursor', __('Cannot move cursor in db file', 'fw'));
         }
         $params = array('search' => array(), 'replace' => array());
         /**
          * Note: rtrim(..., '/') is used to prevent wrong link replace
          *       'http://abc.com/img.jpg' -> 'http://def.comimg.jpg'
          * Note: First links should be the longest, to prevent the short part replace
          *       when the long part must be replaced (thus making wrong replace)
          */
         $search_replace = array();
         /**
          * This parameter (fix) was added after extension release
          * so some demo installs may not have it
          */
         if (isset($state['params']['wp_upload_dir_baseurl'])) {
             $wp_upload_dir = wp_upload_dir();
             $search_replace[rtrim($state['params']['wp_upload_dir_baseurl'], '/')] = rtrim($wp_upload_dir['baseurl'], '/');
             unset($wp_upload_dir);
         }
         $search_replace[rtrim($state['params']['siteurl'], '/')] = rtrim(get_option('siteurl'), '/');
         $search_replace[rtrim($state['params']['home'], '/')] = rtrim(get_option('home'), '/');
         foreach ($search_replace as $search => $replace) {
             $search_replace[fw_get_url_without_scheme($search)] = fw_get_url_without_scheme($replace);
         }
         foreach ($search_replace as $search => $replace) {
             if ($search === $replace) {
                 continue;
             }
             foreach (array($search => $replace, json_encode($search) => json_encode($replace)) as $search => $replace) {
                 $params['search'][] = $search;
                 $params['replace'][] = $replace;
                 $params['search'][] = str_replace('/', '\\/', $search);
                 $params['replace'][] = str_replace('/', '\\/', $replace);
                 $params['search'][] = str_replace('/', '\\\\/', $search);
                 $params['replace'][] = str_replace('/', '\\\\/', $replace);
                 $params['search'][] = str_replace('/', '\\\\\\/', $search);
                 $params['replace'][] = str_replace('/', '\\\\\\/', $replace);
             }
         }
         unset($search_replace, $search, $replace);
         $utf8mb4_is_supported = defined('DB_CHARSET') && DB_CHARSET === 'utf8mb4';
         $started_time = time();
         $timeout = fw_ext('backups')->get_timeout() - 7;
         while (time() - $started_time < $timeout) {
             if ($line = $fo->current()) {
                 if (is_null($line = json_decode($line, true))) {
                     $fo = null;
                     return new WP_Error('line_decode_fail', sprintf(__('Failed to decode line %d from db file.', 'fw') . ' ' . fw_get_json_last_error_message(), $state['step'] + 1));
                 }
                 switch ($line['type']) {
                     case 'table':
                         if (!$state['tables'][$line['data']['name']]) {
                             break;
                             // skip
                         }
                         $tmp_table_name = $this->get_tmp_table_prefix() . $line['data']['name'];
                         if (false === $wpdb->query('DROP TABLE IF EXISTS ' . esc_sql($tmp_table_name))) {
                             $fo = null;
                             return new WP_Error('tmp_table_drop_fail', sprintf(__('Failed to drop tmp table %s', 'fw'), $tmp_table_name));
                         }
                         $sql = 'CREATE TABLE `' . esc_sql($tmp_table_name) . "` (\n";
                         $cols_sql = array();
                         foreach ($line['data']['columns'] as $col_name => $col_opts) {
                             $cols_sql[] = '`' . esc_sql($col_name) . '` ' . ($utf8mb4_is_supported ? $col_opts : str_replace('utf8mb4', 'utf8', $col_opts));
                         }
                         foreach ($line['data']['indexes'] as $index) {
                             $cols_sql[] = $index;
                         }
                         $sql .= implode(", \n", $cols_sql);
                         unset($cols_sql);
                         $sql .= ') ' . ($utf8mb4_is_supported ? $line['data']['opts'] : str_replace('utf8mb4', 'utf8', $line['data']['opts']));
                         if (false === $wpdb->query($sql)) {
                             $fo = null;
                             return new WP_Error('tmp_table_create_fail', sprintf(__('Failed to create tmp table %s', 'fw'), $tmp_table_name));
                         }
                         unset($sql);
                         break;
                     case 'row':
                         if (!isset($state['tables'][$line['data']['table']])) {
                             $fo = null;
                             return new WP_Error('invalid_table', sprintf(__('Tried to insert data in table that was not imported %s', 'fw'), $line['data']['table']));
                         } elseif (!$state['tables'][$line['data']['table']]) {
                             break;
                             // the table was skipped
                         } elseif ('options' === $line['data']['table'] && apply_filters('fw_ext_backups_db_restore_exclude_option', false, $line['data']['row']['option_name'], $state['full'])) {
                             break;
                         }
                         $tmp_table_name = $this->get_tmp_table_prefix() . $line['data']['table'];
                         if (!empty($params['search'])) {
                             $this->array_str_replace_recursive($params['search'], $params['replace'], $line['data']['row']);
                         }
                         if (isset($state['params']['wpdb_prefix'])) {
                             $column = $search = null;
                             switch ($line['data']['table']) {
                                 case 'options':
                                     $column = 'option_name';
                                     $search = array('user_roles');
                                     break;
                                 case 'usermeta':
                                     $column = 'meta_key';
                                     $search = array('capabilities', 'user_level', 'dashboard_quick_press_last_post_id', 'user-settings', 'user-settings-time');
                                     break;
                             }
                             if ($column && $search) {
                                 foreach ($search as $name) {
                                     if (substr($line['data']['row'][$column], -strlen($name)) === $name && substr($line['data']['row'][$column], 0, strlen($state['params']['wpdb_prefix'])) === $state['params']['wpdb_prefix']) {
                                         $line['data']['row'][$column] = $wpdb->prefix . substr($line['data']['row'][$column], strlen($state['params']['wpdb_prefix']));
                                     }
                                 }
                             }
                         }
                         /**
                          * Insert pieces of rows to prevent mysql error when inserting very big strings.
                          * Do this only if table has index column so we can do 'UPDATE ... WHERE index_column = %s'
                          */
                         if ($index_column = $this->get_index_column($tmp_table_name)) {
                             /**
                              * Tested, and maximum value which works is 950000
                              * but may be tables with many columns with big strings
                              * so set this value lower to be sure the limit is not reached.
                              */
                             $value_max_length = 500000;
                             $update_count = 0;
                             $index_column_value = $line['data']['row'][$index_column];
                             $row_lengths = array();
                             while ($line['data']['row']) {
                                 $row = array();
                                 foreach (array_keys($line['data']['row']) as $column_name) {
                                     $row[$column_name] = mb_substr($line['data']['row'][$column_name], 0, $value_max_length);
                                     $row_length = mb_strlen($row[$column_name]);
                                     if (!isset($row_lengths[$column_name])) {
                                         $row_lengths[$column_name] = mb_strlen($line['data']['row'][$column_name]);
                                     }
                                     /**
                                      * The string was cut between a slashed character, for e.g. \" or \\
                                      * Append next characters until the slashing is closed
                                      */
                                     while (($last_char = mb_substr($row[$column_name], -1)) === '\\' && $row_length < $row_lengths[$column_name]) {
                                         $row[$column_name] .= mb_substr($line['data']['row'][$column_name], $row_length - 1, 1);
                                         $row_length++;
                                         // do not call mb_strlen() on every loop
                                     }
                                     $line['data']['row'][$column_name] = mb_substr($line['data']['row'][$column_name], $row_length);
                                     if (empty($line['data']['row'][$column_name])) {
                                         unset($line['data']['row'][$column_name]);
                                     }
                                 }
                                 if ($update_count) {
                                     $set_sql = array();
                                     foreach (array_keys($row) as $column_name) {
                                         $set_sql[] = '`' . esc_sql($column_name) . '` = CONCAT( `' . esc_sql($column_name) . '`' . ' , ' . $wpdb->prepare('%s', $row[$column_name]) . ')';
                                     }
                                     $set_sql = implode(', ', $set_sql);
                                     $sql = implode(" \n", array("UPDATE {$tmp_table_name} SET", $set_sql, 'WHERE `' . esc_sql($index_column) . '` = ' . $wpdb->prepare('%s', $index_column_value)));
                                 } else {
                                     $sql = implode(" \n", array("INSERT INTO {$tmp_table_name} (", '`' . implode('`, `', array_map('esc_sql', array_keys($row))) . '`', ") VALUES (", implode(', ', array_map(array($this, '_wpdb_prepare_string'), $row)), ")"));
                                 }
                                 if (false === $wpdb->query($sql)) {
                                     $fo = null;
                                     return new WP_Error('insert_fail', sprintf(__('Failed insert row from line %d', 'fw'), $state['step'] + 1));
                                 }
                                 unset($sql);
                                 $update_count++;
                             }
                         } else {
                             $sql = implode(" \n", array("INSERT INTO {$tmp_table_name} (", '`' . implode('`, `', array_map('esc_sql', array_keys($line['data']['row']))) . '`', ") VALUES (", implode(', ', array_map(array($this, '_wpdb_prepare_string'), $line['data']['row'])), ")"));
                             if (false === $wpdb->query($sql)) {
                                 $fo = null;
                                 return new WP_Error('insert_fail', sprintf(__('Failed insert row from line %d', 'fw'), $state['step'] + 1));
                             }
                             unset($sql);
                         }
                         break;
                     case 'param':
                         break;
                     default:
                         $fo = null;
                         return new WP_Error('invalid_json_type', sprintf(__('Invalid json type %s in db file', 'fw'), $line['type']));
                 }
             } elseif ($line === false && !$fo->eof()) {
                 $fo = null;
                 return new WP_Error('line_read_fail', __('Cannot read line from db file', 'fw'));
             } else {
                 $fo = null;
                 $state['step'] = 0;
                 $state['task'] = 'keep:options';
                 return $state;
             }
             $state['step']++;
             $fo->next();
         }
         $fo = null;
     } elseif ($state['task'] === 'keep:options') {
         if ($state['full'] && !isset($state['tables']['options'])) {
             // on full backup nothing is kept
         } else {
             $keep_options = array_merge(fw_ext('backups')->get_config('db.restore.keep_options'), apply_filters('fw_ext_backups_db_restore_keep_options', array(), $state['full']));
             $started_time = time();
             $timeout = fw_ext('backups')->get_timeout() - 7;
             // restore array pointer position
             if ($state['step']) {
                 while (($option_name = key($keep_options)) && $option_name !== $state['step']) {
                     next($keep_options);
                 }
                 if (empty($option_name)) {
                     return new WP_Error('keep_options_continue_fail', __('Failed to restore options keeping step', 'fw'));
                 }
             } else {
                 $state['step'] = key($keep_options);
             }
             do {
                 $tmp_options_table = esc_sql($this->get_tmp_table_prefix() . 'options');
                 while (time() - $started_time < $timeout) {
                     if ($row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", $state['step']), ARRAY_A)) {
                         $wpdb->query($wpdb->prepare("DELETE FROM {$tmp_options_table} WHERE option_name = %s", $state['step']));
                         /**
                          * Prevent error: Duplicate entry '90' for key 'PRIMARY' for query INSERT INTO ...options
                          * Option id will be auto incremented on insert
                          */
                         unset($row['option_id']);
                         if (false === $wpdb->query("INSERT INTO {$tmp_options_table} ( \n" . '`' . implode('`, `', array_map('esc_sql', array_keys($row))) . "`  \n" . ") VALUES ( \n" . implode(', ', array_map(array($this, '_wpdb_prepare_string'), $row)) . " \n" . ')')) {
                             return new WP_Error('option_keep_fail', sprintf(__('Failed to keep option: %s', 'fw'), $state['step']));
                         }
                     }
                     next($keep_options);
                     if (is_null($state['step'] = key($keep_options))) {
                         break 2;
                     }
                 }
                 return $state;
             } while (false);
         }
         $state['step'] = 0;
         $state['task'] = 'replace';
         return $state;
     } elseif ($state['task'] === 'replace') {
         /**
          * P.P. We can't rename tables one by one, that can cause errors on next request (db corrupt)
          *      so the only solution is to rename all table at once
          *      and hope that the execution will not exceed timeout limit
          * P.P.S. Table rename should be fast http://dba.stackexchange.com/a/53850
          */
         $current_tables = $this->get_tables();
         $rename_sql = array();
         $drop_sql = array();
         foreach ($state['tables'] as $name => $restored) {
             if ($restored) {
                 if (isset($current_tables[$name])) {
                     // drop only if exists. to prevent sql error
                     $drop_sql[] = esc_sql($wpdb->prefix . $name);
                 }
                 $rename_sql[] = esc_sql($this->get_tmp_table_prefix() . $name) . ' TO ' . esc_sql($wpdb->prefix . $name);
             }
         }
         if (!empty($rename_sql)) {
             if (!empty($drop_sql)) {
                 $drop_sql = "DROP TABLE \n" . implode(" , \n", $drop_sql);
                 if (!$wpdb->query($drop_sql)) {
                     return new WP_Error('tables_drop_fail', __('Tables drop failed', 'fw'));
                 }
             }
             $rename_sql = "RENAME TABLE \n" . implode(" , \n", $rename_sql);
             $wpdb->query($rename_sql);
             // RENAME query doesn't return bool, so use the below method to detect error
             if ($rename_sql === $wpdb->last_query && $wpdb->last_error) {
                 return new WP_Error('tables_rename_fail', __('Tables rename failed.', 'fw') . ' ' . $wpdb->last_error);
             }
         }
         wp_cache_flush();
         return true;
     } else {
         return new WP_Error('invalid_sub_task', sprintf(__('Invalid sub task %s', 'fw'), $state['task']));
     }
     return $state;
 }
 private function do_import(array $args, array $state)
 {
     global $wpdb;
     try {
         $fo = new SplFileObject($args['dir'] . '/database.json.txt');
     } catch (RuntimeException $e) {
         $fo = null;
         return new WP_Error('cannot_open_file', __('Cannot open db file', 'fw'));
     }
     try {
         $fo->seek($state['step']);
     } catch (RuntimeException $e) {
         $fo = null;
         return new WP_Error('cannot_move_file_cursor', __('Cannot move cursor in db file', 'fw'));
     }
     $params = array('search' => array(), 'replace' => array());
     /**
      * Note: rtrim(..., '/') is used to prevent wrong link replace
      *       'http://abc.com/img.jpg' -> 'http://def.comimg.jpg'
      * Note: First links should be the longest, to prevent the short part replace
      *       when the long part must be replaced (thus making wrong replace)
      */
     $search_replace = array();
     /**
      * This parameter (fix) was added after extension release
      * so some demo installs may not have it
      */
     if (isset($state['params']['wp_upload_dir_baseurl'])) {
         $wp_upload_dir = wp_upload_dir();
         $search_replace[rtrim($state['params']['wp_upload_dir_baseurl'], '/')] = rtrim($wp_upload_dir['baseurl'], '/');
         unset($wp_upload_dir);
     }
     foreach (array('siteurl', 'home') as $_wp_option) {
         $search_replace[rtrim($state['params'][$_wp_option], '/')] = rtrim(get_option($_wp_option), '/');
     }
     foreach ($search_replace as $search => $replace) {
         $search_replace[$old_url = fw_get_url_without_scheme($search)] = $new_url = fw_get_url_without_scheme($replace);
         /**
          * If imported url is 'http://hello' and current url is 'http://hello.site.com'
          * after str_replace() all urls will be broken 'http://hello.site.com.site.com.site.com.site.com'
          */
         if (strlen($old_url) !== strlen($new_url) && preg_match('/^' . preg_quote($old_url, '/') . '/', $new_url)) {
             return new WP_Error('url_replace_fail', sprintf(__('Imported url "%s" is prefix of current url', 'fw'), $search));
         }
     }
     // Add all possible combinations of encoding
     foreach ($search_replace as $search => $replace) {
         if ($search === $replace) {
             continue;
         }
         $_search_replace = array($search => $replace, json_encode($search) => json_encode($replace));
         /**
          * Loop through all available shortcodes coders
          * Fixes https://github.com/ThemeFuse/Unyson-Backups-Extension/issues/23
          */
         if ($shortcodes_ext = fw_ext('shortcodes')) {
             /** @var FW_Extension_Shortcodes $shortcodes_ext */
             $_search_replace_coders = array();
             foreach ($shortcodes_ext->get_attr_coder() as $coder) {
                 foreach ($_search_replace as $search => $replace) {
                     /**
                      * Shortcodes atts can contain other encoded shortcodes
                      * so the values will be encoded multiple times
                      */
                     for ($i = 0; $i < 3; $i++) {
                         if (!is_wp_error($search = $coder->encode(array('x' => $search), 'x', 0)) && isset($search['x']) && ($search = $search['x']) && (!is_wp_error($replace = $coder->encode(array('x' => $replace), 'x', 0)) && isset($replace['x']) && ($replace = $replace['x']))) {
                             $_search_replace_coders[$search] = $replace;
                         } else {
                             break;
                         }
                     }
                 }
             }
             $_search_replace = array_merge($_search_replace, $_search_replace_coders);
             unset($shortcodes_ext, $_search_replace_coders);
         }
         foreach ($_search_replace as $search => $replace) {
             $params['search'][] = $search;
             $params['replace'][] = $replace;
             $params['search'][] = str_replace('/', '\\/', $search);
             $params['replace'][] = str_replace('/', '\\/', $replace);
             $params['search'][] = str_replace('/', '\\\\/', $search);
             $params['replace'][] = str_replace('/', '\\\\/', $replace);
             $params['search'][] = str_replace('/', '\\\\\\/', $search);
             $params['replace'][] = str_replace('/', '\\\\\\/', $replace);
         }
         unset($_search_replace);
     }
     unset($search_replace, $search, $replace);
     $replace_option_names = array();
     $filter_data = array();
     if (is_child_theme() && !empty($state['params']['stylesheet']) && $state['params']['stylesheet'] !== get_stylesheet() && $state['params']['stylesheet'] !== get_template()) {
         $filter_data['stylesheet'] = $state['params']['stylesheet'];
         $replace_option_names['theme_mods_' . $state['params']['stylesheet']] = 'theme_mods_' . get_stylesheet();
     }
     if (!empty($state['params']['template']) && $state['params']['template'] !== $state['params']['stylesheet'] && $state['params']['template'] !== get_template() && $state['params']['template'] !== get_stylesheet()) {
         $filter_data['template'] = $state['params']['template'];
         $replace_option_names['theme_mods_' . $state['params']['template']] = 'theme_mods_' . get_template();
     }
     if (!empty($filter_data)) {
         $replace_option_names = array_merge(apply_filters('fw_ext_backups_db_restore_option_names_replace', array(), $filter_data), $replace_option_names);
     }
     unset($filter_data);
     $utf8mb4_is_supported = defined('DB_CHARSET') && DB_CHARSET === 'utf8mb4';
     $max_time = time() + fw_ext('backups')->get_timeout(-$this->get_timeout_padding());
     while (time() < $max_time) {
         if ($line = $fo->current()) {
             if (is_null($line = json_decode($line, true))) {
                 $fo = null;
                 return new WP_Error('line_decode_fail', sprintf(__('Failed to decode line %d from db file.', 'fw') . ' ' . fw_get_json_last_error_message(), $state['step'] + 1));
             }
             switch ($line['type']) {
                 case 'table':
                     if (!$state['tables'][$line['data']['name']]) {
                         break;
                         // skip
                     }
                     $tmp_table_name = $this->get_tmp_table_prefix() . $line['data']['name'];
                     if (strlen($tmp_table_name) > 64) {
                         // http://stackoverflow.com/a/6868316/1794248
                         return new WP_Error('tmp_table_name_invalid', sprintf(__('Table name is more than 64 characters: %s', 'fw'), $tmp_table_name));
                     }
                     if (false === $wpdb->query('DROP TABLE IF EXISTS ' . esc_sql($tmp_table_name))) {
                         $fo = null;
                         return new WP_Error('tmp_table_drop_fail', sprintf(__('Failed to drop tmp table %s', 'fw'), $tmp_table_name) . ($wpdb->last_error ? '. ' . $wpdb->last_error : ''));
                     }
                     $sql = 'CREATE TABLE `' . esc_sql($tmp_table_name) . "` (\n";
                     $cols_sql = array();
                     foreach ($line['data']['columns'] as $col_name => $col_opts) {
                         $cols_sql[] = '`' . esc_sql($col_name) . '` ' . ($utf8mb4_is_supported ? $col_opts : str_replace('utf8mb4', 'utf8', $col_opts));
                     }
                     foreach ($line['data']['indexes'] as $index) {
                         $cols_sql[] = $index;
                     }
                     $sql .= implode(", \n", $cols_sql);
                     unset($cols_sql);
                     $sql .= ') ' . ($utf8mb4_is_supported ? $line['data']['opts'] : str_replace('utf8mb4', 'utf8', $line['data']['opts']));
                     if (false === $wpdb->query($sql)) {
                         $fo = null;
                         return new WP_Error('tmp_table_create_fail', sprintf(__('Failed to create tmp table %s', 'fw'), $tmp_table_name) . ($wpdb->last_error ? '. ' . $wpdb->last_error : ''));
                     }
                     unset($sql);
                     break;
                 case 'row':
                     if (!isset($state['tables'][$line['data']['table']])) {
                         $fo = null;
                         return new WP_Error('invalid_table', sprintf(__('Tried to insert data in table that was not imported %s', 'fw'), $line['data']['table']));
                     } elseif (!$state['tables'][$line['data']['table']]) {
                         break;
                         // the table was skipped
                     } elseif ('options' === $line['data']['table'] && apply_filters('fw_ext_backups_db_restore_exclude_option', false, $line['data']['row']['option_name'], $state['full'])) {
                         break;
                     }
                     if (!empty($params['search'])) {
                         $this->array_str_replace_recursive($params['search'], $params['replace'], $line['data']['row']);
                     }
                     if (!empty($replace_option_names) && $line['data']['table'] === 'options') {
                         if (isset($replace_option_names[$line['data']['row']['option_name']])) {
                             $line['data']['row']['option_name'] = $replace_option_names[$line['data']['row']['option_name']];
                         } elseif (in_array($line['data']['row']['option_name'], $replace_option_names, true)) {
                             /**
                              * Skip
                              *
                              * Do not insert this option because it will be (was) inserted on rename/replace (above)
                              * Prevent 'Duplicate entry' error https://github.com/ThemeFuse/Unyson-Backups-Extension/issues/34
                              *
                              * This happens when:
                              * 1) On Content Backup 'stylesheet' or 'template' is 'theme-name/theme-name'
                              *    but in db there is also 'theme-name' (saved before the theme changed its directory)
                              * 2) On Demo Content Install 'stylesheet' or 'template' is 'theme-name'
                              * 3) The script does replace old 'theme-name/theme-name' to current 'theme-name'
                              *    but that 'theme-name' from (1) conflicts with current 'theme-name'
                              * The solution is to ignore/skip 'theme-name' from 1)
                              */
                             break;
                         }
                     }
                     if (isset($state['params']['wpdb_prefix'])) {
                         $column = $search = null;
                         switch ($line['data']['table']) {
                             case 'options':
                                 $column = 'option_name';
                                 $search = array('user_roles');
                                 break;
                             case 'usermeta':
                                 $column = 'meta_key';
                                 $search = array('capabilities', 'user_level', 'dashboard_quick_press_last_post_id', 'user-settings', 'user-settings-time');
                                 break;
                         }
                         if ($column && $search) {
                             foreach ($search as $name) {
                                 if (substr($line['data']['row'][$column], -strlen($name)) === $name && substr($line['data']['row'][$column], 0, strlen($state['params']['wpdb_prefix'])) === $state['params']['wpdb_prefix']) {
                                     $line['data']['row'][$column] = $wpdb->prefix . substr($line['data']['row'][$column], strlen($state['params']['wpdb_prefix']));
                                 }
                             }
                         }
                     }
                     $tmp_table_name = $this->get_tmp_table_prefix() . $line['data']['table'];
                     /**
                      * Insert pieces of rows to prevent mysql error when inserting very big strings.
                      * Do this only if table has index column so we can do 'UPDATE ... WHERE index_column = %s'
                      */
                     if ($index_column = $this->get_index_column($tmp_table_name)) {
                         /**
                          * Tested, and maximum value which works is 950000
                          * but may be tables with many columns with big strings
                          * so set this value lower to be sure the limit is not reached.
                          */
                         $value_max_length = 500000;
                         $update_count = 0;
                         $index_column_value = $line['data']['row'][$index_column];
                         $row_lengths = array();
                         while ($line['data']['row']) {
                             $row = array();
                             foreach (array_keys($line['data']['row']) as $column_name) {
                                 $row[$column_name] = mb_substr($line['data']['row'][$column_name], 0, $value_max_length);
                                 $row_length = mb_strlen($row[$column_name]);
                                 if (!isset($row_lengths[$column_name])) {
                                     $row_lengths[$column_name] = mb_strlen($line['data']['row'][$column_name]);
                                 }
                                 /**
                                  * The string was cut between a slashed character, for e.g. \" or \\
                                  * Append next characters until the slashing is closed
                                  */
                                 while (($last_char = mb_substr($row[$column_name], -1)) === '\\' && $row_length < $row_lengths[$column_name]) {
                                     $row[$column_name] .= mb_substr($line['data']['row'][$column_name], $row_length - 1, 1);
                                     $row_length++;
                                     // do not call mb_strlen() on every loop
                                 }
                                 $line['data']['row'][$column_name] = mb_substr($line['data']['row'][$column_name], $row_length);
                                 if (empty($line['data']['row'][$column_name])) {
                                     unset($line['data']['row'][$column_name]);
                                 }
                             }
                             if ($update_count) {
                                 $set_sql = array();
                                 foreach (array_keys($row) as $column_name) {
                                     $set_sql[] = '`' . esc_sql($column_name) . '` = CONCAT( `' . esc_sql($column_name) . '`' . ' , ' . $wpdb->prepare('%s', $row[$column_name]) . ')';
                                 }
                                 $set_sql = implode(', ', $set_sql);
                                 $sql = implode(" \n", array("UPDATE {$tmp_table_name} SET", $set_sql, 'WHERE `' . esc_sql($index_column) . '` = ' . $wpdb->prepare('%s', $index_column_value)));
                             } else {
                                 $sql = implode(" \n", array("INSERT INTO {$tmp_table_name} (", '`' . implode('`, `', array_map('esc_sql', array_keys($row))) . '`', ") VALUES (", implode(', ', array_map(array($this, '_wpdb_prepare_string'), $row)), ")"));
                             }
                             if (false === $wpdb->query($sql)) {
                                 if (!$update_count && $wpdb->last_error && (strpos($wpdb->last_error, 'Duplicate') !== false && strpos($wpdb->last_error, '\\x00') !== false)) {
                                     break;
                                 } else {
                                     $fo = null;
                                     return new WP_Error('insert_fail', sprintf(__('Failed to insert row from line %d into table %s', 'fw'), $state['step'] + 1, $tmp_table_name) . ($wpdb->last_error ? '. ' . $wpdb->last_error : ''));
                                 }
                             }
                             unset($sql);
                             $update_count++;
                         }
                     } else {
                         $sql = implode(" \n", array("INSERT INTO {$tmp_table_name} (", '`' . implode('`, `', array_map('esc_sql', array_keys($line['data']['row']))) . '`', ") VALUES (", implode(', ', array_map(array($this, '_wpdb_prepare_string'), $line['data']['row'])), ")"));
                         if (false === $wpdb->query($sql)) {
                             $fo = null;
                             return new WP_Error('insert_fail', sprintf(__('Failed to insert row from line %d into table %s', 'fw'), $state['step'] + 1, $tmp_table_name) . ($wpdb->last_error ? '. ' . $wpdb->last_error : ''));
                         }
                         unset($sql);
                     }
                     break;
                 case 'param':
                     break;
                 default:
                     $fo = null;
                     return new WP_Error('invalid_json_type', sprintf(__('Invalid json type %s in db file', 'fw'), $line['type']));
             }
         } elseif ($line === false && !$fo->eof()) {
             $fo = null;
             return new WP_Error('line_read_fail', __('Cannot read line from db file', 'fw'));
         } else {
             $fo = null;
             $state['step'] = 0;
             $state['task'] = 'keep:options';
             return $state;
         }
         $state['step']++;
         $fo->next();
     }
     $fo = null;
     return $state;
 }