variables = $variables; $this->rules = $rules; $this->types = $types; } public function export() { $items = array(); $offset = 0; $batch_size = 0; do { $batch = $this->variables->all( array( 'number' => 500, 'offset' => $offset, ) ); foreach ( $batch as $variable ) { $items[] = $this->export_item( $variable ); } $batch_size = count( $batch ); $offset += 500; } while ( 500 === $batch_size ); return array( 'schema' => 'glovario-export', 'version' => self::SCHEMA_VERSION, 'generated_at' => gmdate( 'c' ), 'variables' => $items, ); } public function export_file( $format = 'json' ) { if ( 'json' !== $format ) { return new \WP_Error( 'unsupported_export_format', __( 'Select a supported export format.', 'glovario' ) ); } return array( 'content' => wp_json_encode( $this->export(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ), 'extension' => 'json', 'mime' => 'application/json; charset=utf-8', ); } public function import( $json, $strategy = 'skip', $dry_run = true ) { $data = json_decode( $json, true, 32 ); if ( JSON_ERROR_NONE !== json_last_error() ) { /* translators: %s: JSON parser error. */ return new \WP_Error( 'invalid_json', sprintf( __( 'Invalid JSON: %s', 'glovario' ), json_last_error_msg() ) ); } return $this->import_data( $data, $strategy, $dry_run ); } public function import_file( $content, $format = 'json', $strategy = 'skip', $dry_run = true ) { if ( 'json' !== $format ) { return new \WP_Error( 'unsupported_import_format', __( 'Upload a supported import file.', 'glovario' ) ); } return $this->import( $content, $strategy, $dry_run ); } public function import_data( $data, $strategy = 'skip', $dry_run = true ) { $validation = $this->validate( $data ); if ( is_wp_error( $validation ) ) { return $validation; } $report = array( 'created' => 0, 'updated' => 0, 'skipped' => 0, 'rules' => 0, 'dry_run' => (bool) $dry_run, // Image variables whose picture is not in this site's Media Library. 'images_missing' => array(), ); $apply = function () use ( $data, $strategy, $dry_run, $report ) { $current_report = $report; if ( ! $dry_run ) { do_action( 'glovario_before_import_batch', $data, $strategy ); } foreach ( $data['variables'] as $item ) { $existing = $this->variables->find_by_key( $item['key'] ); if ( $existing && 'overwrite' !== $strategy ) { ++$current_report['skipped']; continue; } $rule_count = isset( $item['rules'] ) && is_array( $item['rules'] ) ? count( $item['rules'] ) : 0; if ( 'image' === $item['type'] ) { if ( ! empty( $item['image_url'] ) && '' === $this->local_image( $item['value'], $item['image_url'] ) ) { $current_report['images_missing'][] = $item['key']; } foreach ( (array) ( $item['rules'] ?? array() ) as $rule ) { if ( ! empty( $rule['alternative_image_url'] ) && '' === $this->local_image( $rule['alternative_value'] ?? '', $rule['alternative_image_url'] ) ) { // "shop_logo (Christmas logo)": the rule that needs a picture. $current_report['images_missing'][] = $item['key'] . ' (' . sanitize_text_field( $rule['name'] ?? '' ) . ')'; } } } if ( $dry_run ) { ++$current_report[ $existing ? 'updated' : 'created' ]; $current_report['rules'] += $rule_count; continue; } $value = (string) $item['value']; if ( 'image' === $item['type'] && ! empty( $item['image_url'] ) ) { // Media Library IDs differ between sites: find the same picture here. $value = $this->local_image( $value, $item['image_url'] ); } $row = array( 'variable_key' => $item['key'], 'label' => sanitize_text_field( $item['label'] ), 'type' => $item['type'], 'value' => $this->types->sanitize( $value, $item['type'] ), 'description' => sanitize_textarea_field( $item['description'] ?? '' ), ); if ( array_key_exists( 'category', $item ) ) { $row['category_id'] = $this->category_id( $item['category'] ); } $result = $existing ? $this->variables->update( $existing['id'], $row ) : $this->variables->insert( $row ); if ( is_wp_error( $result ) ) { return $result; } ++$current_report[ $existing ? 'updated' : 'created' ]; $extended = $this->after_import_item( $result, $item ); if ( is_wp_error( $extended ) ) { return $extended; } $current_report['rules'] += $rule_count; } if ( ! $dry_run ) { do_action( 'glovario_after_import_batch', $data, $strategy ); } return $current_report; }; return $dry_run ? $apply() : Transaction::run( $apply ); } protected function export_item( $variable ) { $item = array( 'key' => $variable['variable_key'], 'label' => $variable['label'], 'type' => $variable['type'], 'value' => $variable['value'], 'description' => $variable['description'], 'category' => $this->category_name( $variable['category_id'] ?? 0 ), ); $is_image = 'image' === $variable['type']; if ( $is_image ) { $item['image_url'] = $this->image_url( $variable['value'] ); } $item['rules'] = array_map( function ( $rule ) use ( $is_image ) { $exported = array_intersect_key( $rule, array_flip( array( 'name', 'alternative_value', 'enabled', 'priority', 'conditions' ) ) ); if ( $is_image ) { $exported['alternative_image_url'] = $this->image_url( $rule['alternative_value'] ); } return $exported; }, $this->rules->for_variable( $variable['id'] ) ); return $item; } private function category_name( $category_id ) { $category = $category_id ? ( new CategoryRepository() )->find( $category_id ) : null; return $category ? $category['name'] : ''; } /** * Category ID for an imported category name, creating the category when needed. * * @param string $name Category name; empty for none. * @return int */ private function category_id( $name ) { $name = sanitize_text_field( (string) $name ); if ( '' === $name ) { return 0; } $categories = new CategoryRepository(); foreach ( $categories->all() as $category ) { if ( 0 === strcasecmp( $category['name'], $name ) || sanitize_title( $name ) === $category['slug'] ) { return (int) $category['id']; } } $created = $categories->insert( array( 'name' => $name ) ); return is_wp_error( $created ) ? 0 : (int) $created['id']; } private function image_url( $attachment_id ) { $url = absint( $attachment_id ) ? wp_get_attachment_url( absint( $attachment_id ) ) : false; return $url ? $url : ''; } /** * The attachment on this site that holds the exported picture, or an empty * string when it is not in this Media Library. * * @param string $exported_id Attachment ID on the exporting site. * @param string $url Its file URL on the exporting site. * @return string */ protected function local_image( $exported_id, $url ) { $url = esc_url_raw( (string) $url ); $found = $url ? attachment_url_to_postid( $url ) : 0; $path = (string) wp_parse_url( $url, PHP_URL_PATH ); if ( ! $found && false !== strpos( $path, '/uploads/' ) ) { // Another host (staging → live): the same file under this site's uploads. $relative = substr( $path, strrpos( $path, '/uploads/' ) + strlen( '/uploads/' ) ); $found = attachment_url_to_postid( trailingslashit( wp_get_upload_dir()['baseurl'] ) . $relative ); } $name = wp_basename( $path ); if ( ! $found && '' !== $name ) { // Uploaded again here under another folder: same file name. $matches = get_posts( array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'posts_per_page' => 1, 'fields' => 'ids', 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Runs once per imported image. array( 'key' => '_wp_attached_file', 'value' => '/' . $name, 'compare' => 'LIKE', ), ), ) ); $found = $matches ? (int) $matches[0] : 0; if ( ! $found ) { $file = absint( $exported_id ) ? get_attached_file( absint( $exported_id ) ) : ''; $found = $file && wp_basename( $file ) === $name ? absint( $exported_id ) : 0; } } return $found && TypeRegistry::is_image( $found ) ? (string) $found : ''; } protected function validate( $data ) { if ( ! is_array( $data ) || 'glovario-export' !== ( $data['schema'] ?? '' ) || self::SCHEMA_VERSION !== (int) ( $data['version'] ?? 0 ) || ! isset( $data['variables'] ) || ! is_array( $data['variables'] ) ) { return new \WP_Error( 'invalid_schema', /* translators: Glovario is the product name; do not translate it. */ __( 'The file is not a supported Glovario export.', 'glovario' ) ); } $seen = array(); foreach ( $data['variables'] as $index => $item ) { $key = $item['key'] ?? ''; if ( is_string( $key ) && BuiltinVariables::is_reserved_key( $key ) ) { /* translators: %s: reserved variable key. */ return new \WP_Error( 'reserved_key', sprintf( __( 'The key %s uses the reserved glovario_builtin_ namespace.', 'glovario' ), $key ) ); } if ( ! is_string( $key ) || ! preg_match( '/^[a-z0-9_-]+$/', $key ) || isset( $seen[ $key ] ) ) { /* translators: %d: one-based variable position in import. */ return new \WP_Error( 'invalid_key', sprintf( __( 'Variable %d has an invalid or duplicate key.', 'glovario' ), $index + 1 ) ); } $seen[ $key ] = true; $type = $item['type'] ?? ''; $label_ok = isset( $item['label'] ) && is_string( $item['label'] ) && '' !== trim( $item['label'] ); if ( ! $this->types->exists( $type ) || ! $label_ok || ! isset( $item['value'] ) || ! is_scalar( $item['value'] ) || ( isset( $item['description'] ) && ! is_string( $item['description'] ) ) ) { /* translators: %s: variable key. */ return new \WP_Error( 'invalid_variable', sprintf( __( 'Variable %s has invalid fields.', 'glovario' ), $key ) ); } foreach ( array( 'category', 'image_url' ) as $field ) { if ( isset( $item[ $field ] ) && ( ! is_string( $item[ $field ] ) || strlen( $item[ $field ] ) > 2048 ) ) { /* translators: %s: variable key. */ return new \WP_Error( 'invalid_variable', sprintf( __( 'Variable %s has invalid fields.', 'glovario' ), $key ) ); } } // An exported picture is matched by its URL later; its ID belongs to the other site. $mapped_image = 'image' === $type && ! empty( $item['image_url'] ); $value_check = $mapped_image ? true : $this->types->validate( (string) $item['value'], $type ); if ( is_wp_error( $value_check ) ) { return $value_check; } $extended = $this->validate_item( $item, $type ); if ( is_wp_error( $extended ) ) { return $extended; } } return true; } protected function validate_item( $item, $type ) { if ( ! isset( $item['rules'] ) ) { return true; } if ( ! is_array( $item['rules'] ) ) { return new \WP_Error( 'invalid_rules', __( 'Imported rules must be a list.', 'glovario' ) ); } foreach ( $item['rules'] as $rule ) { if ( ! is_array( $rule ) || ! isset( $rule['alternative_value'], $rule['conditions'] ) || ! is_array( $rule['conditions'] ) ) { /* translators: %s: variable key. */ return new \WP_Error( 'invalid_rule', sprintf( __( 'Variable %s contains an invalid rule.', 'glovario' ), $item['key'] ) ); } $rule_value_check = 'image' === $type && ! empty( $rule['alternative_image_url'] ) ? true : $this->types->validate( (string) $rule['alternative_value'], $type ); if ( is_wp_error( $rule_value_check ) ) { return $rule_value_check; } $conditions_check = ( new ConditionValidator() )->validate( $rule['conditions'] ); if ( is_wp_error( $conditions_check ) ) { return $conditions_check; } } return true; } protected function after_import_item( $variable, $item ) { if ( ! array_key_exists( 'rules', $item ) ) { return true; } $rules = array(); foreach ( $item['rules'] as $rule ) { $conditions = ( new ConditionValidator() )->validate( $rule['conditions'] ); if ( is_wp_error( $conditions ) ) { return $conditions; } $alternative = (string) ( $rule['alternative_value'] ?? '' ); if ( 'image' === $item['type'] && ! empty( $rule['alternative_image_url'] ) ) { $alternative = $this->local_image( $alternative, $rule['alternative_image_url'] ); } $rules[] = array( 'name' => sanitize_text_field( $rule['name'] ?? '' ), 'alternative_value' => $this->types->sanitize( $alternative, $item['type'] ), 'enabled' => empty( $rule['enabled'] ) ? 0 : 1, 'priority' => (int) ( $rule['priority'] ?? 10 ), 'conditions' => $conditions, ); } return $this->rules->replace_for_variable( $variable['id'], $rules ); } }