| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| <?php | ||
|
|
||
| namespace cm4all\wp\impex; | ||
|
|
||
| // Exit if accessed directly. | ||
| if (!defined('ABSPATH')) { | ||
| exit(); | ||
| } | ||
|
|
||
| require_once ABSPATH . '/wp-admin/includes/export.php'; | ||
|
|
||
| require_once __DIR__ . '/interface-impex-named-item.php'; | ||
| require_once __DIR__ . '/class-impex.php'; | ||
|
|
||
| use cm4all\wp\impex\Impex; | ||
|
|
||
| function __AttachmentsExportProviderCallback(array $options, ImpexExportTransformationContext $transformationContext): \Generator | ||
| { | ||
| $attachments = \get_posts(['post_type' => 'attachment', 'numberposts' => -1, 'post_status' => null, 'post_parent' => null]); | ||
|
|
||
| foreach ($attachments as $attachment) { | ||
| $mediaRelPath = substr(\get_attached_file($attachment->ID), strlen(\wp_get_upload_dir()['basedir']) + 1); | ||
|
|
||
| $targetFile = $transformationContext->path . '/' . $mediaRelPath; | ||
|
|
||
| global $wp_filesystem; | ||
| \WP_Filesystem(); | ||
|
|
||
| // ensure target file directory exists | ||
| $wp_filesystem->exists(dirname($targetFile)) || \wp_mkdir_p(dirname($targetFile)); | ||
| $successfulCopied = $wp_filesystem->copy(\get_attached_file($attachment->ID), $targetFile); | ||
|
|
||
| yield [ | ||
| Impex::SLICE_TAG => AttachmentsExporter::SLICE_TAG, | ||
| Impex::SLICE_VERSION => AttachmentsExporter::VERSION, | ||
| Impex::SLICE_TYPE => Impex::SLICE_TYPE_RESOURCE, | ||
| Impex::SLICE_META => [ | ||
| 'name' => $attachment->post_title, | ||
| Impex::SLICE_META_ENTITY => AttachmentsExporter::SLICE_META_ENTITY_ATTACHMENT, | ||
| 'options' => $options, | ||
| 'data' => (array)$attachment, | ||
| ], | ||
| Impex::SLICE_DATA => $mediaRelPath, | ||
| ]; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @TODO: convert to enum if enums once are available in PHP | ||
| */ | ||
| interface AttachmentsExporter | ||
| { | ||
| const SLICE_TAG = 'attachment'; | ||
| const SLICE_META_ENTITY_ATTACHMENT = 'attachment'; | ||
|
|
||
| const PROVIDER_NAME = self::class; | ||
|
|
||
| const VERSION = '1.0.0'; | ||
| } | ||
|
|
||
| function __registerAttachmentsExportProvider() | ||
| { | ||
| $provider = Impex::getInstance()->Export->addProvider(AttachmentsExporter::PROVIDER_NAME, __NAMESPACE__ . '\__AttachmentsExportProviderCallback'); | ||
| return $provider; | ||
| } | ||
|
|
||
| \add_action( | ||
| hook_name: Impex::WP_ACTION_REGISTER_PROVIDERS, | ||
| callback: __NAMESPACE__ . '\__registerAttachmentsExportProvider', | ||
| ); | ||
|
|
||
| \add_action( | ||
| hook_name: 'rest_api_init', | ||
| callback: function () { | ||
| require_once __DIR__ . '/class-impex-export-rest-controller.php'; | ||
|
|
||
| \add_filter( | ||
| hook_name: ImpexExportRESTController::WP_FILTER_EXPORT_SLICE_REST_MARSHAL, | ||
| callback: function (array $serialized_slice, ImpexExportTransformationContext $transformationContext) { | ||
| if ( | ||
| $serialized_slice[Impex::SLICE_TAG] === AttachmentsExporter::SLICE_TAG && | ||
| $serialized_slice[Impex::SLICE_META][Impex::SLICE_META_ENTITY] === AttachmentsExporter::SLICE_META_ENTITY_ATTACHMENT && | ||
| $serialized_slice[Impex::SLICE_TYPE] === Impex::SLICE_TYPE_RESOURCE | ||
| ) { | ||
| $serialized_slice['_links'] ??= []; | ||
|
|
||
| $serialized_slice['_links']['self'] ??= []; | ||
|
|
||
| $serialized_slice['_links']['self'][] = [ | ||
| 'href' => $transformationContext->url . '/' . $serialized_slice[Impex::SLICE_DATA], | ||
| 'tag' => AttachmentsExporter::SLICE_TAG, | ||
| 'provider' => AttachmentsExporter::PROVIDER_NAME, | ||
| ]; | ||
| } | ||
| return $serialized_slice; | ||
| }, | ||
| accepted_args: 2, | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| \add_action( | ||
| hook_name: Impex::WP_ACTION_ENQUEUE_IMPEX_PROVIDER_SCRIPT, | ||
| callback: function ($client_asset_handle, $in_footer) { | ||
| \cm4all\wp\impex\wp_enqueue_script( | ||
| handle: strtolower(str_replace('\\', '-', AttachmentsExporter::PROVIDER_NAME)), | ||
| deps: [$client_asset_handle, $client_asset_handle . '-debug'], | ||
| pluginRelativePath: 'dist/wp.impex.extension.export.attachments.js', | ||
| in_footer: $in_footer | ||
| ); | ||
| }, | ||
| accepted_args: 2, | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * caveat : wildcard selectors will work only for regular (non-temporary) tables. | ||
| * you can workaround this by providing the explizit table/view names instead of a wildcard | ||
| */ | ||
|
|
||
| namespace cm4all\wp\impex; | ||
|
|
||
| // Exit if accessed directly. | ||
| if (!defined('ABSPATH')) { | ||
| exit(); | ||
| } | ||
|
|
||
| require_once ABSPATH . '/wp-admin/includes/export.php'; | ||
|
|
||
| require_once __DIR__ . '/interface-impex-named-item.php'; | ||
| require_once __DIR__ . '/class-impex.php'; | ||
|
|
||
| use cm4all\wp\impex\Impex; | ||
|
|
||
| function __DbTablesExportProviderCallback(array $options, ImpexExportTransformationContext $transformationContext): \Generator | ||
| { | ||
| global $wpdb; | ||
| static $db_table_names = null; | ||
|
|
||
| $selectors = $options[DbTablesExporter::OPTION_SELECTOR] ?? null; | ||
|
|
||
| // ensure selector is valid | ||
| if (!(is_array($selectors) || is_string($selectors))) { | ||
| throw new ImpexExportRuntimeException(sprintf('dont know how to handle export option DbTablesExporter::OPTION_SELECTOR(=%s)', json_encode($selectors))); | ||
| } | ||
|
|
||
| // normalize selector to type array | ||
| if (is_string($selectors)) { | ||
| $selectors = [$selectors]; | ||
| } | ||
|
|
||
| foreach ($selectors as $selector) { | ||
| if (str_contains($selector, '[') || str_contains($selector, '*') || str_contains($selector, '?')) { | ||
| $db_table_names ??= $wpdb->get_col('SHOW TABLES') ?? []; | ||
|
|
||
| foreach ($db_table_names as $db_table_name) { | ||
| if (fnmatch($wpdb->prefix . $selector, $db_table_name)) { | ||
| yield from __dbTableProvider(array_merge($options, [DbTablesExporter::OPTION_SELECTOR => substr($db_table_name, strlen($wpdb->prefix))])); | ||
| } | ||
| } | ||
| } else { | ||
| yield from __dbTableProvider(array_merge($options, [DbTablesExporter::OPTION_SELECTOR => $selector])); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function __dbTableProvider(array $options): \Generator | ||
| { | ||
| $table = $options[DbTablesExporter::OPTION_SELECTOR]; | ||
|
|
||
| global $wpdb; | ||
|
|
||
| // see https://stackoverflow.com/questions/4294507/how-to-dump-mysql-table-structure-without-data-with-a-sql-query/12448816 | ||
| $table_ddl = $wpdb->get_var("SHOW CREATE TABLE {$wpdb->prefix}$table", 1,); | ||
| // normalize table name | ||
| $table_ddl = str_replace($wpdb->prefix, '%prefix%', $table_ddl,); | ||
| // inject table create failover | ||
| $table_ddl = str_replace('CREATE TABLE', 'CREATE TABLE IF NOT EXISTS', $table_ddl,); | ||
|
|
||
| // yield tabel ddl chunk | ||
| yield [ | ||
| Impex::SLICE_TAG => DbTablesExporter::SLICE_TAG, | ||
| Impex::SLICE_VERSION => DbTablesExporter::VERSION, | ||
| Impex::SLICE_META => [ | ||
| 'name' => $table, | ||
| 'entity' => DbTablesExporter::SLICE_META_ENTITY_TABLE, | ||
| 'options' => $options, | ||
| ], | ||
| Impex::SLICE_DATA => $table_ddl, | ||
| ]; | ||
|
|
||
| // yield a slice for each db rows chunk | ||
| $chunk_max_items = $options[DbTablesExporter::OPTION_SLICE_MAX_ITEMS] ?? DbTablesExporter::OPTION_SLICE_MAX_ITEMS_DEFAULT; | ||
| if ($chunk_max_items > 0) { | ||
| $rows = $wpdb->get_results("SELECT * from {$wpdb->prefix}$table"); | ||
| $chunk_count = ceil(count($rows) / $chunk_max_items); | ||
|
|
||
| for ($chunk = 0; $chunk < $chunk_count; $chunk++) { | ||
| // index of first item in this chunk | ||
| $chunk_ofs = $chunk * $chunk_max_items; | ||
| // amount of items to yield in this chunk | ||
| $chunk_item_count = min($chunk_max_items, count($rows) - $chunk_ofs); | ||
|
|
||
| yield [ | ||
| Impex::SLICE_TAG => DbTablesExporter::SLICE_TAG, | ||
| Impex::SLICE_VERSION => DbTablesExporter::VERSION, | ||
| Impex::SLICE_META => [ | ||
| 'name' => $table, | ||
| 'entity' => DbTablesExporter::SLICE_META_ENTITY_ROWS, | ||
| ], | ||
| Impex::SLICE_DATA => array_slice($rows, $chunk_ofs, $chunk_item_count) | ||
| ]; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @TODO: convert to enum if enums once are available in PHP | ||
| */ | ||
| interface DbTablesExporter | ||
| { | ||
| const SLICE_TAG = 'db-table'; | ||
| const SLICE_META_ENTITY_TABLE = 'db-table-entity-table'; | ||
| const SLICE_META_ENTITY_ROWS = 'db-table-entity-rows'; | ||
|
|
||
| const OPTION_SELECTOR = 'db-tables-export-option-selector'; | ||
|
|
||
| const OPTION_SLICE_MAX_ITEMS = 'db-tables-export-option-chunk-max-items'; | ||
| const OPTION_SLICE_MAX_ITEMS_DEFAULT = 50; | ||
|
|
||
| const PROVIDER_NAME = self::class; | ||
|
|
||
| const VERSION = '1.0.0'; | ||
| } | ||
|
|
||
| function __registerDbTablesExportProvider() | ||
| { | ||
| $provider = Impex::getInstance()->Export->addProvider(DbTablesExporter::PROVIDER_NAME, __NAMESPACE__ . '\__DbTablesExportProviderCallback'); | ||
| return $provider; | ||
| } | ||
|
|
||
| \add_action( | ||
| hook_name: Impex::WP_ACTION_REGISTER_PROVIDERS, | ||
| callback: __NAMESPACE__ . '\__registerDbTablesExportProvider', | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| <?php | ||
|
|
||
| namespace cm4all\wp\impex; | ||
|
|
||
| // Exit if accessed directly. | ||
| if (!defined('ABSPATH')) { | ||
| exit(); | ||
| } | ||
|
|
||
| require_once ABSPATH . '/wp-admin/includes/export.php'; | ||
|
|
||
| require_once __DIR__ . '/interface-impex-named-item.php'; | ||
| require_once __DIR__ . '/class-impex.php'; | ||
|
|
||
| use cm4all\wp\impex\Impex; | ||
|
|
||
| function __matchesSelector(string $wpOptionName, array|string $selectors): bool | ||
| { | ||
| // normalize selector to array of selectors | ||
| if (is_string($selectors)) { | ||
| $selectors = [$selectors]; | ||
| } | ||
|
|
||
| if (is_array($selectors)) { | ||
| foreach ($selectors as $selector) { | ||
| if (fnmatch($selector, $wpOptionName)) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| function __WpOptionsExporterProviderCallback(array $options, ImpexExportTransformationContext $transformationContext): \Generator | ||
| { | ||
| $selector = $options[WpOptionsExporter::OPTION_SELECTOR] ?? null; | ||
|
|
||
| // ensure selector is valid | ||
| if (!(is_array($selector) || is_string($selector))) { | ||
| throw new ImpexExportRuntimeException(sprintf('dont know how to handle export option WpOptionsExporter::OPTION_SELECTOR(=%s)', json_encode($selector))); | ||
| } | ||
|
|
||
| $chunk_max_items = $options[WpOptionsExporter::OPTION_SLICE_MAX_ITEMS] ?? WpOptionsExporter::OPTION_SLICE_MAX_ITEMS_DEFAULT; | ||
| $chunks = []; | ||
| $current_chunk = []; | ||
| foreach (\wp_load_alloptions() as $wpOptionName => $wpOptionValue) { | ||
| if (__matchesSelector($wpOptionName, $selector)) { | ||
| // CAVEAT: we cannot use $wpOptionValue since its not automagically deserialized | ||
| // $wpOptionValue transports just the plain serialization string, so we need to utilize | ||
| // \get_option to get the correct value | ||
| // otherwise array and object values will not be coeectly exported | ||
| $current_chunk[$wpOptionName] = \get_option($wpOptionName); | ||
|
|
||
| if (count($current_chunk) === $chunk_max_items) { | ||
| $chunks[] = $current_chunk; | ||
| $current_chunk = []; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (count($current_chunk) > 0) { | ||
| $chunks[] = $current_chunk; | ||
| } | ||
|
|
||
| foreach ($chunks as $chunk) { | ||
| yield [ | ||
| Impex::SLICE_TAG => WpOptionsExporter::SLICE_TAG, | ||
| Impex::SLICE_VERSION => WpOptionsExporter::VERSION, | ||
| Impex::SLICE_META => [ | ||
| Impex::SLICE_META_ENTITY => WpOptionsExporter::SLICE_META_ENTITY_WP_OPTIONS, | ||
| 'options' => $options, | ||
| ], | ||
| Impex::SLICE_DATA => $chunk, | ||
| ]; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @TODO: convert to enum if enums once are available in PHP | ||
| */ | ||
| interface WpOptionsExporter | ||
| { | ||
| const SLICE_TAG = 'wp-options'; | ||
| const SLICE_META_ENTITY_WP_OPTIONS = self::SLICE_TAG; | ||
|
|
||
| const OPTION_SELECTOR = 'wp-options-export-option-selector'; | ||
| const OPTION_SLICE_MAX_ITEMS = 'wp-options-export-option-chunk-max-items'; | ||
| const OPTION_SLICE_MAX_ITEMS_DEFAULT = 50; | ||
|
|
||
| const PROVIDER_NAME = self::class; | ||
|
|
||
| const VERSION = '1.0.0'; | ||
| } | ||
|
|
||
| function __registerWpOptionsExportProvider() | ||
| { | ||
| $provider = Impex::getInstance()->Export->addProvider(WpOptionsExporter::PROVIDER_NAME, __NAMESPACE__ . '\__WpOptionsExporterProviderCallback'); | ||
| return $provider; | ||
| } | ||
|
|
||
| \add_action( | ||
| hook_name: Impex::WP_ACTION_REGISTER_PROVIDERS, | ||
| callback: __NAMESPACE__ . '\__registerWpOptionsExportProvider', | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,276 @@ | ||
| <?php | ||
|
|
||
| namespace cm4all\wp\impex; | ||
|
|
||
| // Exit if accessed directly. | ||
| if (!defined('ABSPATH')) { | ||
| exit(); | ||
| } | ||
|
|
||
| require_once ABSPATH . '/wp-admin/includes/import.php'; | ||
|
|
||
| require_once __DIR__ . '/interface-impex-named-item.php'; | ||
| require_once __DIR__ . '/class-impex.php'; | ||
|
|
||
| use cm4all\wp\impex\Impex; | ||
|
|
||
| /** | ||
| * @TODO: we can declare the contents of an array (https://bleepcoder.com/plugin-php/617134322/broken-phpdoc-for-closures) | ||
| * // @param array{a: int, b: string} $bar | ||
| */ | ||
| function __AttachmentImportProviderCallback(array $slice, array $options, ImpexImportTransformationContext $transformationContext): bool | ||
| { | ||
| if ($slice[Impex::SLICE_TAG] === AttachmentsExporter::SLICE_TAG) { | ||
| if (($slice[Impex::SLICE_TYPE] === Impex::SLICE_TYPE_RESOURCE) && $slice[Impex::SLICE_META][Impex::SLICE_META_ENTITY] === AttachmentsExporter::SLICE_META_ENTITY_ATTACHMENT) { | ||
| if ($slice[Impex::SLICE_VERSION] !== AttachmentsExporter::VERSION) { | ||
| throw new ImpexImportRuntimeException(sprintf('Dont know how to import slice(tag="%s", version="%s") : unsupported version. current version is "%s"', AttachmentsExporter::SLICE_TAG, $slice[Impex::SLICE_VERSION], AttachmentsExporter::VERSION)); | ||
| } | ||
|
|
||
| $attachmentImporter = new __AttachmentImporter($slice, $options, $transformationContext); | ||
| $attachmentImporter(); | ||
|
|
||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| interface AttachmentImporter | ||
| { | ||
| const PROVIDER_NAME = self::class; | ||
|
|
||
| const WP_FILTER_IMPORT_REST_SLICE_UPLOAD_FILE = "AttachmentImporter"; | ||
|
|
||
| const OPTION_OVERWRITE = 'wp-attachment-import-option-overwrite'; | ||
| const OPTION_OVERWRITE_DEFAULT = true; | ||
| } | ||
|
|
||
| function __registerAttachmentImportProvider() | ||
| { | ||
| $provider = Impex::getInstance()->Import->addProvider(AttachmentImporter::PROVIDER_NAME, __NAMESPACE__ . '\__AttachmentImportProviderCallback'); | ||
| return $provider; | ||
| } | ||
|
|
||
| \add_action( | ||
| hook_name: Impex::WP_ACTION_REGISTER_PROVIDERS, | ||
| callback: __NAMESPACE__ . '\__registerAttachmentImportProvider', | ||
| ); | ||
|
|
||
| class __AttachmentImporter | ||
| { | ||
| protected $url_remap = []; | ||
|
|
||
| function __construct(protected array $slice, protected array $options, protected ImpexImportTransformationContext $transformationContext) | ||
| { | ||
| } | ||
|
|
||
| protected function delete_existing_attachments_matching_upload() | ||
| { | ||
| $file_name = basename(parse_url($this->slice[Impex::SLICE_META]['data']['guid'], PHP_URL_PATH)); | ||
|
|
||
| //$uploads = \wp_upload_dir($this->slice[Impex::SLICE_META]['post_date']); | ||
| $post_date = $this->slice[Impex::SLICE_META]['data']['post_date']; | ||
| $uploads = \wp_upload_dir($post_date); | ||
| if (!($uploads && false === $uploads['error'])) { | ||
| return; | ||
| } | ||
|
|
||
| $url = $uploads['url'] . "/$file_name"; | ||
|
|
||
| global $wpdb; | ||
|
|
||
| foreach ($wpdb->get_col($wpdb->prepare("select ID from wp_posts where post_type='attachment' and guid =%s", $url)) as $ID) { | ||
| $success = \wp_delete_attachment($ID, true); | ||
| if ($success == false || $success == null) { | ||
| throw new ImpexImportRuntimeException(sprintf('import attachment : failed to remove existing attachment(ID="%s") referencing with same attachment url(="%s")', $ID, $url)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function __invoke() | ||
| { | ||
| $overwrite = $this->options[AttachmentImporter::OPTION_OVERWRITE] ?? AttachmentImporter::OPTION_OVERWRITE_DEFAULT; | ||
| if ($overwrite) { | ||
| // ensure existing attachments referencing same file are removed before inserting ours | ||
| $this->delete_existing_attachments_matching_upload(); | ||
| } | ||
|
|
||
| $post = $this->slice[Impex::SLICE_META]['data']; | ||
| $url = $post['guid']; | ||
|
|
||
| /* | ||
| code more or less duplicated from wordpress-importer function process_attachment | ||
| https://github.com/WordPress/wordpress-importer/blob/e05f678835c60030ca23c9a186f50999e198a360/src/class-wp-import.php#L1009 | ||
| */ | ||
|
|
||
| $upload = $this->fetch_remote_file($url, $post, $this->slice); | ||
| if (\is_wp_error($upload)) { | ||
| return $upload; | ||
| } | ||
|
|
||
| $post['guid'] = $upload['url']; | ||
| unset($post['ID']); | ||
|
|
||
| // as per wp-admin/includes/upload.php | ||
| $post_id = \wp_insert_attachment($post, $upload['file']); | ||
|
|
||
| // required if non admin user imports attachments | ||
| if (!function_exists('wp_crop_image')) { | ||
| include(ABSPATH . 'wp-admin/includes/image.php'); | ||
| } | ||
|
|
||
| /*$attachment_invalid =*/ | ||
| \wp_update_attachment_metadata($post_id, \wp_generate_attachment_metadata($post_id, $upload['file'])); | ||
|
|
||
| /* | ||
| if ($attachment_invalid === false) { | ||
| return new \WP_Error('wp_update_attachment_metadata', 'attachment metadata invalid'); | ||
| } | ||
| */ | ||
|
|
||
| // remap resized image URLs, works by stripping the extension and remapping the URL stub. | ||
| if (preg_match('!^image/!', $post['post_mime_type'])) { | ||
| $parts = pathinfo($url); | ||
| $name = basename($parts['basename'], ".{$parts['extension']}"); // PATHINFO_FILENAME in PHP 5.2 | ||
|
|
||
| $parts_new = pathinfo($upload['url']); | ||
| $name_new = basename($parts_new['basename'], ".{$parts_new['extension']}"); | ||
|
|
||
| $this->url_remap[$parts['dirname'] . '/' . $name] = $parts_new['dirname'] . '/' . $name_new; | ||
| } | ||
|
|
||
| // @TODO: any change to do this AFTER all images are uploaded ? | ||
| $this->backfill_attachment_urls(); | ||
| } | ||
|
|
||
| /** | ||
| * patched version of wordpress-importer fetch_remote_file() | ||
| */ | ||
| function fetch_remote_file($url, $post, $slice) | ||
| { | ||
| // Extract the file name from the URL. | ||
| $file_name = basename(parse_url($url, PHP_URL_PATH)); | ||
|
|
||
| $uploads = wp_upload_dir($post['post_date']); | ||
| if (!($uploads && false === $uploads['error'])) { | ||
| return new \WP_Error('upload_dir_error', $uploads['error']); | ||
| } | ||
|
|
||
| $file_name = wp_unique_filename($uploads['path'], $file_name); | ||
| $new_file = $uploads['path'] . "/$file_name"; | ||
|
|
||
| // Copy the file to the uploads dir. | ||
| copy( | ||
| $this->transformationContext->path . '/' . $this->slice[Impex::SLICE_DATA], | ||
| $new_file, | ||
| ); | ||
|
|
||
| // Set correct file permissions. | ||
| $stat = stat(dirname($new_file)); | ||
| $perms = $stat['mode'] & 0000666; | ||
| chmod($new_file, $perms); | ||
|
|
||
| $upload = [ | ||
| 'file' => $new_file, | ||
| 'url' => $uploads['url'] . "/$file_name", | ||
| 'type' => $post['post_mime_type'], | ||
| 'error' => false, | ||
| ]; | ||
|
|
||
| // keep track of the old and new urls so we can substitute them later | ||
| $this->url_remap[$url] = $upload['url']; | ||
| $this->url_remap[$post['guid']] = $upload['url']; // r13735, really needed? | ||
| /* | ||
| // keep track of the destination if the remote url is redirected somewhere else | ||
| if (isset($headers['x-final-location']) && $headers['x-final-location'] != $url) { | ||
| $this->url_remap[$headers['x-final-location']] = $upload['url']; | ||
| } | ||
| */ | ||
|
|
||
| return $upload; | ||
| } | ||
|
|
||
| /** | ||
| * patched version of wordpress-importer backfill_attachment_urls() | ||
| * @see https://github.com/WordPress/wordpress-importer/blob/e05f678835c60030ca23c9a186f50999e198a360/src/class-wp-import.php#L1265 | ||
| */ | ||
| // @TODO: any change to do this AFTER all images are uploaded ? | ||
| function backfill_attachment_urls() | ||
| { | ||
| global $wpdb; | ||
| // make sure we do the shortest urls first, in case one is a substring of another | ||
| uksort($this->url_remap, [&$this, 'cmpr_strlen']); | ||
|
|
||
| foreach ($this->url_remap as $from_url => $to_url) { | ||
| // remap urls in post_content | ||
| // replace from_url => to_url and also for block attributes in json notation | ||
| $wpdb->query($wpdb->prepare("UPDATE {$wpdb->posts} SET post_content = REPLACE( REPLACE(post_content, %s, %s), %s, %s)", $from_url, $to_url, json_encode($from_url), json_encode($to_url))); | ||
| // remap enclosure urls | ||
| $result = $wpdb->query($wpdb->prepare("UPDATE {$wpdb->postmeta} SET meta_value = REPLACE(meta_value, %s, %s) WHERE meta_key='enclosure'", $from_url, $to_url)); | ||
| } | ||
| } | ||
|
|
||
| // return the difference in length between two strings | ||
| function cmpr_strlen($a, $b) | ||
| { | ||
| return strlen($a) - strlen($b); | ||
| } | ||
| } | ||
|
|
||
| \add_action( | ||
| hook_name: 'rest_api_init', | ||
| callback: function () { | ||
| require_once __DIR__ . '/class-impex-import-rest-controller.php'; | ||
| \add_filter( | ||
| hook_name: ImpexImportRESTController::WP_FILTER_IMPORT_REST_SLICE_UPLOAD, | ||
| callback: function (array $slice, ImpexImportTransformationContext $transformationContext, \WP_REST_Request $request) { | ||
| if ( | ||
| $slice[Impex::SLICE_TAG] === AttachmentsExporter::SLICE_TAG | ||
| ) { | ||
| $files = $request->get_file_params(); | ||
| if (!is_array($files)) { | ||
| throw new \WP_Error('bad-request', __('Multipart file upload is missing in request', 'cm4all-wp-impex'), ['status' => 400]); | ||
| } | ||
|
|
||
| $attachmentFile = $files[AttachmentImporter::WP_FILTER_IMPORT_REST_SLICE_UPLOAD_FILE] ?? null; | ||
| if ($attachmentFile === null) { | ||
| throw new ImpexImportRuntimeException(sprintf('Multipart file upload "%s" is missing in request', AttachmentImporter::WP_FILTER_IMPORT_REST_SLICE_UPLOAD_FILE)); | ||
| } | ||
|
|
||
| $to = $transformationContext->path . '/' . $slice[Impex::SLICE_DATA]; | ||
| $success = \wp_mkdir_p(dirname($to)); | ||
| $success = rename($attachmentFile["tmp_name"], $to); | ||
|
|
||
| if ($success === null) { | ||
| throw new ImpexImportRuntimeException(sprintf('Failed to move uploaded attachment(=%) to impex import directory : %s', $attachmentFile["tmp_name"], $to)); | ||
| } | ||
|
|
||
| unset($files[AttachmentImporter::WP_FILTER_IMPORT_REST_SLICE_UPLOAD_FILE]); | ||
| $request->set_file_params($files); | ||
| } | ||
| return $slice; | ||
| }, | ||
| accepted_args: 3, | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| \add_action( | ||
| hook_name: Impex::WP_ACTION_ENQUEUE_IMPEX_PROVIDER_SCRIPT, | ||
| callback: function ($client_asset_handle, $in_footer) { | ||
| $HANDLE = strtolower(str_replace('\\', '-', AttachmentImporter::PROVIDER_NAME)); | ||
| \cm4all\wp\impex\wp_enqueue_script( | ||
| handle: $HANDLE, | ||
| deps: [$client_asset_handle, $client_asset_handle . '-debug'], | ||
| pluginRelativePath: 'dist/wp.impex.extension.import.attachment.js', | ||
| in_footer: $in_footer | ||
| ); | ||
|
|
||
| \wp_add_inline_script( | ||
| $HANDLE, | ||
| sprintf('wp.impex.extension.import.attachment.WP_FILTER_IMPORT_REST_SLICE_UPLOAD_FILE=%s;', \wp_json_encode(AttachmentImporter::WP_FILTER_IMPORT_REST_SLICE_UPLOAD_FILE)), | ||
| ); | ||
| }, | ||
| accepted_args: 2, | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| <?php | ||
|
|
||
| namespace cm4all\wp\impex; | ||
|
|
||
| // Exit if accessed directly. | ||
| if (!defined('ABSPATH')) { | ||
| exit(); | ||
| } | ||
|
|
||
| require_once ABSPATH . '/wp-admin/includes/import.php'; | ||
|
|
||
| require_once __DIR__ . '/interface-impex-named-item.php'; | ||
| require_once __DIR__ . '/class-impex.php'; | ||
|
|
||
| use cm4all\wp\impex\Impex; | ||
|
|
||
| function __DbTableImportProviderCallback(array $slice, array $options, ImpexImportTransformationContext $transformationContext): bool | ||
| { | ||
| global $wpdb; | ||
|
|
||
| if ($slice[Impex::SLICE_TAG] === DbTablesExporter::SLICE_TAG) { | ||
| if ($slice[Impex::SLICE_VERSION] !== DbTablesExporter::VERSION) { | ||
| throw new ImpexImportRuntimeException(sprintf('Dont know how to import slice(tag="%s", version="%s") : unsupported version. current version is "%s"', DbTablesExporter::SLICE_TAG, $slice[Impex::SLICE_VERSION], DbTablesExporter::VERSION)); | ||
| } | ||
|
|
||
| // @TODO: assume same database server/connection for now | ||
| $target_wpdb = $wpdb; | ||
|
|
||
| $option_db_tableprefix = $options[DbTableImporter::OPTION_SELECTORPREFIX] ?? $wpdb->prefix; | ||
|
|
||
| /** | ||
| * @TODO: how to handle report | ||
| * @var callable $log | ||
| */ | ||
| $log = function (string $message, mixed $context = null) use ($options) { | ||
| isset($options[Impex::OPTION_LOG]) && call_user_func($options[Impex::OPTION_LOG], $message, $context); | ||
| }; | ||
|
|
||
| $slice_meta = $slice[Impex::SLICE_META]; | ||
| $target_table_name = $option_db_tableprefix . $slice_meta['name']; | ||
|
|
||
| switch ($slice_meta[Impex::SLICE_META_ENTITY]) { | ||
| case DbTablesExporter::SLICE_META_ENTITY_TABLE: { | ||
| $option_db_truncate = $options[DbTableImporter::OPTION_TRUNCATE] ?? DbTableImporter::OPTION_TRUNCATE_DEFAULT; | ||
| $option_db_overwrite_definition = $options[DbTableImporter::OPTION_OVERWRITE_DEFINITION] ?? DbTableImporter::OPTION_OVERWRITE_DEFINITION_DEFAULT; | ||
|
|
||
| $ddl = $slice[Impex::SLICE_DATA]; | ||
|
|
||
| if ($target_wpdb->query('START TRANSACTION') !== false) { | ||
| try { | ||
| // @TODO: should we disable keys before and reenable them afterwards ? 'ALTER TABLE ' . $target_table_name . ' ENABLE KEYS' | ||
|
|
||
| // (optional) drop table | ||
| if ($option_db_overwrite_definition) { | ||
| $queryRetval = $target_wpdb->query("DROP TABLE IF EXISTS $target_table_name"); | ||
| $queryRetval === false && throw new RollbackSignal(); | ||
|
|
||
| $log(sprintf('%s : %s', $target_wpdb->last_query, $queryRetval)); | ||
| } | ||
|
|
||
| // create table if not exists | ||
| $queryRetval = $target_wpdb->query(strtr($ddl, ['%prefix%' => $option_db_tableprefix])); | ||
| $queryRetval === false && throw new RollbackSignal(); | ||
|
|
||
| $log(sprintf('%s : %s', $target_wpdb->last_query, $queryRetval)); | ||
|
|
||
| // (optional) truncate data | ||
| if (!$option_db_overwrite_definition && $option_db_truncate) { | ||
| $queryRetval = $target_wpdb->query($target_wpdb->prepare('TRUNCATE TABLE %s', $target_table_name)); | ||
| $queryRetval === false && throw new RollbackSignal(); | ||
|
|
||
| $log(sprintf('%s : %s', $target_wpdb->last_query, $queryRetval)); | ||
| } | ||
|
|
||
| /* @TODO: check later if the snippet below makes sense in our case | ||
| // we could copy the values "in place" using sql | ||
| // use CREATE TABLE ... LIKE ... to keep keys, defaults, ... | ||
| $ret = $target_wpdb->query( | ||
| 'CREATE TABLE ' . | ||
| $task->arguments->target_table . | ||
| ' LIKE ' . | ||
| $task->arguments->source_table | ||
| ); | ||
| */ | ||
|
|
||
| // should we copy data in place from table to table ? | ||
| if (isset($options[DbTableImporter::OPTION_COPY_DATA_FROM_TABLE_WITH_PREFIX])) { | ||
| $queryRetval = $target_wpdb->query(strtr( | ||
| 'INSERT INTO %target_table% SELECT * FROM %source_table%', | ||
| [ | ||
| '%target_table%' => $target_table_name, | ||
| '%source_table%' => $options[DbTableImporter::OPTION_COPY_DATA_FROM_TABLE_WITH_PREFIX] . $slice_meta['name'], | ||
| ] | ||
| )); | ||
| $queryRetval === false && throw new RollbackSignal(); | ||
|
|
||
| $log(sprintf('%s : %s', $target_wpdb->last_query, $queryRetval)); | ||
| } | ||
|
|
||
| $target_wpdb->query('COMMIT'); | ||
|
|
||
| return true; | ||
| } catch (RollbackSignal $signal) { | ||
| $target_wpdb->query('ROLLBACK'); | ||
| throw $signal; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
| case DbTablesExporter::SLICE_META_ENTITY_ROWS: { | ||
| // if data where already copied from table to table inside the database | ||
| // we can skip proceeding the rows | ||
| if (!isset($options[DbTableImporter::OPTION_COPY_DATA_FROM_TABLE_WITH_PREFIX])) { | ||
|
|
||
| $option_db_overwrite_data = $options[DbTableImporter::OPTION_OVERWRITE_DATA] ?? DbTableImporter::OPTION_OVERWRITE_DATA_DEFAULT; | ||
|
|
||
| $rows = $slice[Impex::SLICE_DATA]; | ||
|
|
||
| // TODO: implement optimized in-db-copy procedure when special option provided | ||
| if (count($rows)) { | ||
| $table_column_names = implode('`, `', array_keys((array)$rows[0])); | ||
|
|
||
| /* | ||
| see https://thispointer.com/insert-into-a-mysql-table-or-update-if-exists/#three | ||
| > REPLACE works similar to INSERT. The difference is: If the new row to be inserted has the same value | ||
| of the PRIMARY KEY or the UNIQUE index as the existing row, in that case, the old row gets deleted first | ||
| before inserting the new one. | ||
| REPLACE INTO customer_data(customer_id, customer_name, customer_place) VALUES(2, "Hevika","Atlanta"); | ||
| */ | ||
| $placeholders = str_repeat('%s, ', count(array_keys((array)$rows[0])) - 1) . '%s'; | ||
| $dml = strtr('%insert_or_replace% INTO `%name%`(`%column_names%`) VALUES(%values_placeholder%)', [ | ||
| '%insert_or_replace%' => $option_db_overwrite_data ? 'REPLACE' : 'INSERT', | ||
| '%name%' => $target_table_name, | ||
| '%column_names%' => $table_column_names, | ||
| '%values_placeholder%' => $placeholders, | ||
| ]); | ||
|
|
||
| foreach ($rows as $row) { | ||
| // @TODO: consider using https://developer.wordpress.org/reference/classes/wpdb/#replace-row | ||
| // instead of populating a sql statement manually - it seems much shorter and easier to read | ||
| $preparedStatement = $target_wpdb->prepare($dml, (array)$row); | ||
|
|
||
| $queryRetval = $target_wpdb->query($preparedStatement); | ||
| if ($queryRetval === false) { | ||
| $log(sprintf('%s : %s', $target_wpdb->last_query, $queryRetval)); | ||
| throw new RollbackSignal(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
| default: { | ||
| throw new ImpexImportRuntimeException(sprintf('dont now how to handle slice meta entity : %s', $slice_meta['entity']), $slice); | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| class RollbackSignal extends ImpexImportRuntimeException | ||
| { | ||
| function __construct($msg = "") | ||
| { | ||
| global $wpdb; | ||
|
|
||
| if ($msg === "" && $wpdb->last_error) { | ||
| parent::__construct("{$wpdb->last_query} : $wpdb->last_error"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| interface DbTableImporter | ||
| { | ||
| const OPTION_TRUNCATE = 'db-table-import-option-truncate'; | ||
| const OPTION_TRUNCATE_DEFAULT = true; | ||
|
|
||
| const OPTION_OVERWRITE_DATA = 'db-table-import-option-overwrite_data'; | ||
| const OPTION_OVERWRITE_DATA_DEFAULT = true; | ||
|
|
||
| const OPTION_COPY_DATA_FROM_TABLE_WITH_PREFIX = 'db-table-import-option-copy-data-from-table-with-prefix'; | ||
|
|
||
| const OPTION_OVERWRITE_DEFINITION = 'db-table-import-option-overwrite-definition'; | ||
| const OPTION_OVERWRITE_DEFINITION_DEFAULT = true; | ||
|
|
||
| const OPTION_SELECTORPREFIX = 'db-table-import-option-tableprefix'; | ||
|
|
||
| const PROVIDER_NAME = self::class; | ||
| } | ||
|
|
||
| function __registerDbTableImportProvider() | ||
| { | ||
| $provider = Impex::getInstance()->Import->addProvider(DbTableImporter::PROVIDER_NAME, __NAMESPACE__ . '\__DbTableImportProviderCallback'); | ||
| return $provider; | ||
| } | ||
|
|
||
| \add_action( | ||
| hook_name: Impex::WP_ACTION_REGISTER_PROVIDERS, | ||
| callback: __NAMESPACE__ . '\__registerDbTableImportProvider', | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| <?php | ||
|
|
||
| namespace cm4all\wp\impex; | ||
|
|
||
| // Exit if accessed directly. | ||
| if (!defined('ABSPATH')) { | ||
| exit(); | ||
| } | ||
|
|
||
| require_once ABSPATH . '/wp-admin/includes/import.php'; | ||
|
|
||
| require_once __DIR__ . '/interface-impex-named-item.php'; | ||
| require_once __DIR__ . '/class-impex.php'; | ||
|
|
||
| use cm4all\wp\impex\Impex; | ||
|
|
||
| function __WpOptionsImportProviderCallback(array $slice, array $options, ImpexImportTransformationContext $transformationContext): bool | ||
| { | ||
| if ($slice[Impex::SLICE_TAG] === WpOptionsExporter::SLICE_TAG) { | ||
| if ($slice[Impex::SLICE_META][Impex::SLICE_META_ENTITY] === WpOptionsExporter::SLICE_META_ENTITY_WP_OPTIONS) { | ||
| if ($slice[Impex::SLICE_VERSION] !== WpOptionsExporter::VERSION) { | ||
| throw new ImpexImportRuntimeException(sprintf('Dont know how to import slice(tag="%s", version="%s") : unsupported version. current version is "%s"', WpOptionsExporter::SLICE_TAG, $slice[Impex::SLICE_VERSION], WpOptionsExporter::VERSION)); | ||
| } | ||
|
|
||
| foreach ($slice[Impex::SLICE_DATA] as $wpOptionName => $wpOptionValue) { | ||
| \update_option($wpOptionName, $wpOptionValue); | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| interface WpOptionsImporter | ||
| { | ||
| const PROVIDER_NAME = self::class; | ||
| } | ||
|
|
||
| function __registerWpOptionsImportProvider() | ||
| { | ||
| $provider = Impex::getInstance()->Import->addProvider(WpOptionsImporter::PROVIDER_NAME, __NAMESPACE__ . '\__WpOptionsImportProviderCallback'); | ||
| return $provider; | ||
| } | ||
|
|
||
| \add_action( | ||
| hook_name: Impex::WP_ACTION_REGISTER_PROVIDERS, | ||
| callback: __NAMESPACE__ . '\__registerWpOptionsImportProvider', | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <?php | ||
|
|
||
| namespace cm4all\wp\impex; | ||
|
|
||
| // Exit if accessed directly. | ||
| if (!defined('ABSPATH')) { | ||
| exit(); | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * @property-read $name | ||
| */ | ||
| interface ImpexNamedItem | ||
| { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| <?php | ||
|
|
||
| namespace cm4all\wp\impex; | ||
|
|
||
| // Exit if accessed directly. | ||
| if (!defined('ABSPATH')) { | ||
| exit(); | ||
| } | ||
|
|
||
| interface ImpexRestController | ||
| { | ||
| const VERSION = '1'; | ||
|
|
||
| const NAMESPACE = 'cm4all-wp-impex/v' . self::VERSION; | ||
|
|
||
| const BASE_URI = '/' . self::NAMESPACE; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| <?php | ||
|
|
||
| namespace cm4all\wp\impex; | ||
|
|
||
| // Exit if accessed directly. | ||
| if (!defined('ABSPATH')) { | ||
| exit(); | ||
| } | ||
|
|
||
| require_once __DIR__ . '/interface-impex-named-item.php'; | ||
| require_once __DIR__ . '/class-impex-runtime-exception.php'; | ||
|
|
||
| trait ImpexSet /* implements \IteratorAggregate */ | ||
| { | ||
| protected array $_items = []; | ||
|
|
||
| protected function _indexByName(string $name): int | ||
| { | ||
| foreach ($this->_items as $index => $item) { | ||
| if ($item->name === $name) { | ||
| return $index; | ||
| } | ||
| } | ||
|
|
||
| return -1; | ||
| } | ||
|
|
||
| public function add(ImpexNamedItem $item): self | ||
| { | ||
| if ($this->has($item->name)) { | ||
| throw new ImpexRuntimeException(sprintf('Cannot add item : An item named "%s" already exists', $item->name)); | ||
| } | ||
|
|
||
| $this->_items[] = $item; | ||
|
|
||
| return $this; | ||
| } | ||
|
|
||
| public function has(string $name): bool | ||
| { | ||
| return $this->_indexByName($name) !== -1; | ||
| } | ||
|
|
||
| public function get(string $name): ImpexNamedItem|null | ||
| { | ||
| $index = $this->_indexByName($name); | ||
| return $index !== -1 ? $this->_items[$index] : null; | ||
| } | ||
|
|
||
| public function remove(string $name): ImpexNamedItem|null | ||
| { | ||
| $index = $this->_indexByName($name); | ||
| if ($index !== -1) { | ||
| $item = $this->_items[$index]; | ||
| array_splice($this->_items, $index, 1); | ||
| return $item; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| public function move(string $name, int $newIndex): bool | ||
| { | ||
| $index = $this->_indexByName($name); | ||
| if ($index !== -1) { | ||
| $namedItem = array_splice($this->_items, $index, 1); | ||
| array_splice($this->_items, $newIndex, 0, $namedItem); | ||
| } | ||
|
|
||
| return $index !== -1; | ||
| } | ||
|
|
||
| /** | ||
| * @see \IteratorAggregate | ||
| */ | ||
| public function getIterator(): \Iterator | ||
| { | ||
| return new \ArrayIterator(array_values($this->_items)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| <?php | ||
|
|
||
| namespace cm4all\wp\impex; | ||
|
|
||
| // Exit if accessed directly. | ||
| if (!defined('ABSPATH')) { | ||
| exit(); | ||
| } | ||
|
|
||
| trait ImpexSingleton | ||
| { | ||
| private static $instance; | ||
|
|
||
| protected function __construct() | ||
| { | ||
| } | ||
|
|
||
| public static function getInstance(): static | ||
| { | ||
| if (!self::$instance) { | ||
| // new self() will refer to the class that uses the trait | ||
| self::$instance = new self(); | ||
| } | ||
|
|
||
| return self::$instance; | ||
| } | ||
|
|
||
| protected function __clone() | ||
| { | ||
| } | ||
| public function __sleep() | ||
| { | ||
| } | ||
| public function __wakeup() | ||
| { | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| <?php | ||
|
|
||
| namespace cm4all\wp\impex\wp_admin\dashboard; | ||
|
|
||
| use function cm4all\wp\impex\enqueueClientAssets; | ||
|
|
||
| // Exit if accessed directly. | ||
| if (!defined('ABSPATH')) { | ||
| exit(); | ||
| } | ||
|
|
||
| require_once __DIR__ . '/wp-wrapper-functions.php'; | ||
|
|
||
| const TITLE = 'Impex'; | ||
| // slug cannot be defined as const since constants need to be available at compile time | ||
| // => https://stackoverflow.com/questions/51000541/php-constant-expression-contains-invalid-operations | ||
| define('IMPEX_SCREEN_PAGE_SLUG', str_replace("\\", '_', __NAMESPACE__)); | ||
| // ATTENTION : the '_page' suffix is important ! | ||
| // otherwise storeing the value via filter 'set-screen-option' will fail | ||
| const SCREEN_OPTION_VERBOSE = IMPEX_SCREEN_PAGE_SLUG . '_' . 'verbose_page'; | ||
|
|
||
| \add_action( | ||
| hook_name: "admin_menu", | ||
| callback: function () { | ||
| $submenu_page_hook_suffix = \add_submenu_page('tools.php', TITLE, TITLE, 'manage_options', IMPEX_SCREEN_PAGE_SLUG, function () { | ||
| require_once ABSPATH . 'wp-admin/admin-header.php'; | ||
|
|
||
| echo sprintf('<div class="wrap" id="%s"></div>', IMPEX_SCREEN_PAGE_SLUG); | ||
|
|
||
| require_once ABSPATH . 'wp-admin/admin-footer.php'; | ||
| }, 0); | ||
|
|
||
| \add_action( | ||
| hook_name: 'load-' . $submenu_page_hook_suffix, | ||
| callback: function () { | ||
| \get_current_screen()->add_help_tab( | ||
| [ | ||
| 'id' => 'help-overview', | ||
| 'title' => __('Overview', 'cm4all-wp-impex'), | ||
| 'content' => ' | ||
| <p> | ||
| Impex provides extensible Import and Export capabilities across plugins / themes by providing a hook mechanism to 3rd-party plugins and themes. | ||
| </p> | ||
| <p> | ||
| Impex supports a pluggable provider interface to export / import custom data. Impex export and import can be customized ab applying filters. | ||
| </p> | ||
| <p> | ||
| By default a self contained, streamable <a href="https://cbor.io/">CBOR</a> archive gets exported (also including media) and vice versa imported. | ||
| </p> | ||
| ', | ||
| ] | ||
| ); | ||
|
|
||
| \get_current_screen()->add_help_tab( | ||
| [ | ||
| 'id' => 'help-export', | ||
| 'title' => __('Export', 'cm4all-wp-impex'), | ||
| 'content' => ' | ||
| TODO | ||
| ' | ||
| ] | ||
| ); | ||
|
|
||
| \get_current_screen()->add_help_tab( | ||
| [ | ||
| 'id' => 'help-import', | ||
| 'title' => __('Import', 'cm4all-wp-impex'), | ||
| 'content' => ' | ||
| TODO | ||
| ' | ||
| ] | ||
| ); | ||
|
|
||
| \get_current_screen()->set_help_sidebar( | ||
| sprintf( | ||
| '<p><strong>%s</strong></p><p>%s</p><p>%s</p>', | ||
| __('For more information:', 'cm4all-wp-impex'), | ||
| __('<a href="https://wordpress.org/support/article/tools-screen/">Documentation on Impex</a>', 'cm4all-wp-impex'), | ||
| __('<a href="https://wordpress.org/support/">Support</a>', 'cm4all-wp-impex') | ||
| ) | ||
| ); | ||
|
|
||
| $IN_FOOTER = true; | ||
| $IMPEX_CLIENT_HANDLE = enqueueClientAssets($IN_FOOTER); | ||
| \cm4all\wp\impex\wp_enqueue_script( | ||
| handle: IMPEX_SCREEN_PAGE_SLUG, | ||
| pluginRelativePath: 'dist/wp.impex.dashboard.js', | ||
| deps: [$IMPEX_CLIENT_HANDLE, 'wp-element', 'wp-api-fetch', 'wp-url', 'wp-i18n', 'wp-components', 'wp-data', 'wp-core-data'], | ||
| in_footer: $IN_FOOTER | ||
| ); | ||
| \wp_set_script_translations( | ||
| handle: IMPEX_SCREEN_PAGE_SLUG, | ||
| domain: $IMPEX_CLIENT_HANDLE, | ||
| path: plugin_dir_path(__DIR__) . 'languages' | ||
| ); | ||
|
|
||
| \cm4all\wp\impex\wp_enqueue_style( | ||
| handle: IMPEX_SCREEN_PAGE_SLUG, | ||
| pluginRelativePath: 'dist/wp.impex.dashboard.css', | ||
| deps: [$IMPEX_CLIENT_HANDLE, 'wp-components'] | ||
| ); | ||
|
|
||
| /* | ||
| prevent loading wp admin forms.css since it breaks gutenberg component styles | ||
| wp_register_style doesnt overwrite exiting style registrations so that we need to | ||
| - remove the original style | ||
| - add a dummy style handle for 'forms' | ||
| */ | ||
| \wp_deregister_style(handle: 'forms'); | ||
| \wp_register_style(handle: 'forms', src: ''); | ||
| }, | ||
| ); | ||
| }, | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| <?php | ||
|
|
||
| namespace cm4all\wp\impex; | ||
|
|
||
| /** | ||
| * contains various utility functions wrapping general WordPress functions | ||
| */ | ||
|
|
||
| // Exit if accessed directly. | ||
| if (!defined('ABSPATH')) { | ||
| exit(); | ||
| } | ||
|
|
||
| /** | ||
| * Retrieves a URL within the wp-impex plugin directory. | ||
| * | ||
| * @param string $path Optional. Extra path appended to the end of the URL, including | ||
| * the relative directory if $plugin is supplied. Default empty. | ||
| * | ||
| * @see \plugins_url | ||
| */ | ||
| function plugins_url(string $path = ''): string | ||
| { | ||
| return \plugins_url($path, __DIR__); | ||
| } | ||
|
|
||
| /** | ||
| * Get the filesystem directory path (with trailing slash) for the wp-impex plugin. | ||
| * | ||
| * @param string $path (optional) The filename of the plugin | ||
| * @return string the filesystem path of the directory contained in wp-impex plugin. | ||
| * | ||
| * @see \plugin_dir_path | ||
| */ | ||
| function plugin_dir_path(string $path = ''): string | ||
| { | ||
| return \plugin_dir_path(__DIR__) . $path; | ||
| } | ||
|
|
||
| /** | ||
| * Register a new script. | ||
| * | ||
| * Registers a script to be enqueued later using the wp_enqueue_script() function. | ||
| * | ||
| * @param string $handle Name of the script. Should be unique. | ||
| * @param string $pluginRelativePath path of the script relative to the plugin root. | ||
| * If source is set to false, script is an alias of other scripts it depends on. | ||
| * @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array. | ||
| * @param bool $in_footer Optional. Whether to enqueue the script before </body> instead of in the <head>. | ||
| * Default 'false'. | ||
| * @return bool Whether the script has been registered. True on success, false on failure. | ||
| * | ||
| * @see \wp_register_script | ||
| */ | ||
| function wp_register_script(string $handle, string $pluginRelativePath, array $deps = [], bool $in_footer = false): bool | ||
| { | ||
| if (defined(SCRIPT_DEBUG) && !SCRIPT_DEBUG) { | ||
| $pluginRelativePath = preg_replace('/\.js$/', '-min.js', $pluginRelativePath); | ||
| } | ||
|
|
||
| return \wp_register_script($handle, plugins_url($pluginRelativePath), $deps, filemtime(plugin_dir_path($pluginRelativePath)), $in_footer); | ||
| } | ||
|
|
||
| /** | ||
| * Enqueue a script. | ||
| * | ||
| * @param string $handle Name of the script. Should be unique. | ||
| * @param string $pluginRelativePath path of the script relativeto the pluginroot. | ||
| * @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array. | ||
| * @param bool $in_footer Optional. Whether to enqueue the script before </body> instead of in the <head>. | ||
| * Default 'false'. | ||
| * | ||
| * \wp_enqueue_script | ||
| */ | ||
| function wp_enqueue_script(string $handle, string $pluginRelativePath, array $deps = [], bool $in_footer = false) | ||
| { | ||
| wp_register_script($handle, $pluginRelativePath, $deps, $in_footer); | ||
|
|
||
| return \wp_enqueue_script($handle); | ||
| } | ||
|
|
||
| /** | ||
| * Register a CSS stylesheet. | ||
| * | ||
| * @param string $handle Name of the stylesheet. Should be unique. | ||
| * @param string $pluginRelativePath path of the stylesheet relative to the plugin root. | ||
| * If source is set to false, stylesheet is an alias of other stylesheets it depends on. | ||
| * @param string[] $deps Optional. An array of registered stylesheet handles this stylesheet depends on. Default empty array. | ||
| * @param string $media Optional. The media for which this stylesheet has been defined. | ||
| * Default 'all'. Accepts media types like 'all', 'print' and 'screen', or media queries like | ||
| * '(orientation: portrait)' and '(max-width: 640px)'. | ||
| * @return bool Whether the style has been registered. True on success, false on failure. | ||
| * | ||
| * @see \wp_register_style | ||
| */ | ||
| function wp_register_style(string $handle, string $pluginRelativePath, array $deps = [], string $media = 'all'): bool | ||
| { | ||
| if (defined(SCRIPT_DEBUG) && !SCRIPT_DEBUG) { | ||
| $pluginRelativePath = preg_replace('/\.css$/', '-min.css', $pluginRelativePath); | ||
| } | ||
|
|
||
| return \wp_register_style($handle, plugins_url($pluginRelativePath), $deps, filemtime(plugin_dir_path($pluginRelativePath)), $media); | ||
| } | ||
|
|
||
| /** | ||
| * Enqueue a CSS stylesheet. | ||
| * | ||
| * Registers the style if source provided (does NOT overwrite) and enqueues. | ||
| * | ||
| * @param string $handle Name of the stylesheet. Should be unique. | ||
| * @param string $pluginRelativePath path of the stylesheet relative to the plugin root. | ||
| * @param string[] $deps Optional. An array of registered stylesheet handles this stylesheet depends on. Default empty array. | ||
| * @param string $media Optional. The media for which this stylesheet has been defined. | ||
| * Default 'all'. Accepts media types like 'all', 'print' and 'screen', or media queries like | ||
| * '(orientation: portrait)' and '(max-width: 640px)'. | ||
| * | ||
| * @see \wp_enqueue_style | ||
| */ | ||
| function wp_enqueue_style(string $handle, string $pluginRelativePath, array $deps = [], string $media = 'all') | ||
| { | ||
| wp_register_style($handle, $pluginRelativePath, $deps, $media); | ||
|
|
||
| return \wp_enqueue_style($handle); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,293 @@ | ||
| # Copyright (C) 2021 Lars Gersmann, CM4all | ||
| # This file is distributed under the same license as the cm4all-wp-impex plugin. | ||
| msgid "" | ||
| msgstr "" | ||
| "Project-Id-Version: cm4all-wp-impex 1.0.0\n" | ||
| "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/cm4all-wp-impex\n" | ||
| "POT-Creation-Date: 2022-01-24T08:24:24+00:00\n" | ||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||
| "Language-Team: LANGUAGE <LL@li.org>\n" | ||
| "Language: \n" | ||
| "MIME-Version: 1.0\n" | ||
| "Content-Type: text/plain; charset=UTF-8\n" | ||
| "Content-Transfer-Encoding: 8bit\n" | ||
| "X-Generator: WP-CLI 2.5.0\n" | ||
| "X-Domain: cm4all-wp-impex\n" | ||
|
|
||
| #. Plugin Name of the plugin | ||
| msgid "cm4all-wp-impex" | ||
| msgstr "" | ||
|
|
||
| #. Plugin URI of the plugin | ||
| msgid "http://dev.intern.cm-ag/trinity/research/cm4all-wp-impex" | ||
| msgstr "" | ||
|
|
||
| #. Author of the plugin | ||
| msgid "Lars Gersmann, CM4all" | ||
| msgstr "" | ||
|
|
||
| #. Author URI of the plugin | ||
| msgid "https://cm4all.com" | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-export-rest-controller.php:142 | ||
| #: inc/class-impex-export-rest-controller.php:161 | ||
| msgid "could not find export by id" | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-export-rest-controller.php:175 | ||
| #: inc/class-impex-import-rest-controller.php:253 | ||
| msgid "body expected to be a json object" | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-export-rest-controller.php:180 | ||
| msgid "export profile not found" | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-export-rest-controller.php:208 | ||
| #: inc/class-impex-import-rest-controller.php:286 | ||
| msgid "Failed to update export" | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-export-rest-controller.php:227 | ||
| #: inc/class-impex-import-rest-controller.php:305 | ||
| msgid "removing impex export failed" | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-export-rest-controller.php:351 | ||
| msgid "Unique identifier for the export." | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-export-rest-controller.php:358 | ||
| msgid "The options used to create the export." | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-export-rest-controller.php:366 | ||
| msgid "The name of the export profile to use." | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-export-rest-controller.php:375 | ||
| msgid "The date of the export." | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-export-rest-controller.php:382 | ||
| msgid "The user login of the user creating the export." | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-export-rest-controller.php:388 | ||
| msgid "The human readable name of the export" | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-export-rest-controller.php:395 | ||
| msgid "The human readable description of the export" | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-import-rest-controller.php:156 | ||
| msgid "Multipart parameter slice is missing" | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-import-rest-controller.php:173 | ||
| msgid "Multipart parameter \"slice\" is expected to contain json" | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-import-rest-controller.php:189 | ||
| #: inc/class-impex-import-rest-controller.php:214 | ||
| #: inc/class-impex-import-rest-controller.php:239 | ||
| msgid "could not find import by id" | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-import-rest-controller.php:429 | ||
| msgid "Unique identifier for the import." | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-import-rest-controller.php:436 | ||
| msgid "The options used to create the import." | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-import-rest-controller.php:444 | ||
| msgid "The name of the import profile to use." | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-import-rest-controller.php:453 | ||
| msgid "The date of the import." | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-import-rest-controller.php:460 | ||
| msgid "The user login of the user creating the import." | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-import-rest-controller.php:466 | ||
| msgid "The name of the import" | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-import-rest-controller.php:473 | ||
| msgid "The description of the import" | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-profile-rest-controller.php:171 | ||
| msgid "Unique identifier for the profile." | ||
| msgstr "" | ||
|
|
||
| #: inc/class-impex-profile-rest-controller.php:177 | ||
| msgid "Human readable description of the profile." | ||
| msgstr "" | ||
|
|
||
| #: inc/impex-import-extension-attachment.php:233 | ||
| msgid "Multipart file upload is missing in request" | ||
| msgstr "" | ||
|
|
||
| #: inc/wp-dashboard-contributions.php:39 | ||
| msgid "Overview" | ||
| msgstr "" | ||
|
|
||
| #: inc/wp-dashboard-contributions.php:57 dist/wp.impex.dashboard.js:354 | ||
| msgid "Export" | ||
| msgstr "(en_US) Export" | ||
|
|
||
| #: inc/wp-dashboard-contributions.php:67 dist/wp.impex.dashboard.js:546 | ||
| msgid "Import" | ||
| msgstr "(en_US) Import" | ||
|
|
||
| #: inc/wp-dashboard-contributions.php:77 | ||
| msgid "For more information:" | ||
| msgstr "(en_US) For more information:" | ||
|
|
||
| #: inc/wp-dashboard-contributions.php:78 | ||
| msgid "" | ||
| "<a href=\"https://wordpress.org/support/article/tools-screen/" | ||
| "\">Documentation on Impex</a>" | ||
| msgstr "" | ||
| "(en_US) <a href=\"https://wordpress.org/support/article/tools-screen/" | ||
| "\">Documentation on Impex</a>" | ||
|
|
||
| #: inc/wp-dashboard-contributions.php:79 | ||
| msgid "<a href=\"https://wordpress.org/support/\">Support</a>" | ||
| msgstr "(en_US) <a href=\"https://wordpress.org/support/\">Support</a>" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:188 | ||
| msgid "Name" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:189 | ||
| msgid "Name should be short and human readable" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:193 | ||
| msgid "Description" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:194 | ||
| msgid "Description may contain more expressive information describing the item" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:285 | ||
| msgid "Creating snapshot" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:327 | ||
| msgid "Downloading snapshot" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:356 | ||
| msgid "Create snapshot" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:362 | ||
| msgid "Export Profile" | ||
| msgstr "(en_US) Export Profile" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:367 | ||
| msgid "Select an Export profile" | ||
| msgstr "(en_US) Select an Export profile" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:376 | ||
| msgid "" | ||
| "Export profiles define which Wordpress data should be extracted to the " | ||
| "snapshot" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:381 | ||
| msgid "Create Snapshot" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:389 | ||
| msgid "Download snapshot" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:390 | ||
| msgid "Additional actions on this export" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:393 dist/wp.impex.dashboard.js:587 | ||
| msgid "Edit" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:398 | ||
| #, fuzzy | ||
| msgid "Edit export" | ||
| msgstr "(en_US) Export" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:407 dist/wp.impex.dashboard.js:601 | ||
| msgid "Delete" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:412 | ||
| msgid "Delete export" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:464 | ||
| msgid "Importing data into Wordpress ..." | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:511 | ||
| msgid "Uploading snapshot" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:548 | ||
| msgid "Upload snapshot to Wordpress" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:554 | ||
| #, fuzzy | ||
| msgid "Import Profile" | ||
| msgstr "(en_US) Export Profile" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:559 | ||
| #, fuzzy | ||
| msgid "Select an Import profile" | ||
| msgstr "(en_US) Select an Export profile" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:568 | ||
| msgid "" | ||
| "Import profiles define which Wordpress data should be consumed from the " | ||
| "snapshot" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:574 | ||
| msgid "Upload snapshot" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:583 | ||
| msgid "Import uploaded snapshot" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:584 | ||
| msgid "Additional actions on this import" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:592 | ||
| msgid "Edit import snapshot" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:606 | ||
| msgid "Delete import" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:607 | ||
| msgid "Are you really sure to delete import" | ||
| msgstr "" | ||
|
|
||
| #: dist/wp.impex.dashboard.js:632 | ||
| msgid "Impex" | ||
| msgstr "" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * Plugin Name: cm4all-wp-impex | ||
| * Plugin URI: http://dev.intern.cm-ag/trinity/research/cm4all-wp-impex | ||
| * Description : Impex contributes extendable Import / Export functionality to WordPress | ||
| * Version: 1.0.0 | ||
| * Tags: import, export, migration | ||
| * Requires PHP: 8.0 | ||
| * Requires at least: 5.7 | ||
| * Author: Lars Gersmann, CM4all | ||
| * Author URI: https://cm4all.com | ||
| * Domain Path: /languages | ||
| **/ | ||
|
|
||
| namespace cm4all\wp\impex; | ||
|
|
||
| // Exit if accessed directly. | ||
| if (!defined('ABSPATH')) { | ||
| exit(); | ||
| } | ||
|
|
||
| require_once __DIR__ . '/inc/class-impex.php'; | ||
|
|
||
| require_once __DIR__ . '/inc/impex-export-extension-content.php'; | ||
| require_once __DIR__ . '/inc/impex-import-extension-content.php'; | ||
| require_once __DIR__ . '/inc/impex-export-extension-attachments.php'; | ||
| require_once __DIR__ . '/inc/impex-import-extension-attachment.php'; | ||
| require_once __DIR__ . '/inc/impex-export-extension-db-tables.php'; | ||
| require_once __DIR__ . '/inc/impex-import-extension-db-table.php'; | ||
| require_once __DIR__ . '/inc/impex-export-extension-wp-options.php'; | ||
| require_once __DIR__ . '/inc/impex-import-extension-wp-options.php'; | ||
| require_once __DIR__ . '/inc/class-impex-export-profile-rest-controller.php'; | ||
| require_once __DIR__ . '/inc/class-impex-import-profile-rest-controller.php'; | ||
| require_once __DIR__ . '/inc/class-impex-export-rest-controller.php'; | ||
| require_once __DIR__ . '/inc/class-impex-import-rest-controller.php'; | ||
|
|
||
| require_once __DIR__ . '/inc/wp-dashboard-contributions.php'; | ||
|
|
||
| /** | ||
| * db table creation/upgrade mechanism: | ||
| * @see https://codex.wordpress.org/Creating_Tables_with_Plugins | ||
| */ | ||
|
|
||
| \register_activation_hook(__FILE__, function () { | ||
| // ensure required PHP function fnmatch() exists | ||
| if (!function_exists('fnmatch')) { | ||
| \wp_die('<h3>Plugin activation aborted</h3><p>The <strong>cm4all-wp-impex</strong> plugin requires PHP function <a href="https://www.php.net/manual/en/function.fnmatch.php" target="_blank"><code>fnmatch</code></a> to be available.</p>', 'Plugin Activation Error', ['response' => 500, 'back_link' => TRUE]); | ||
| } | ||
|
|
||
| Impex::getInstance()->__install(); | ||
| }); | ||
|
|
||
| // disable wordpress update notifications in the cloud to suppress php errors in the cloud | ||
| // @TODO: remove when its deployed in the cm4all-wordpress plugin | ||
| if (str_ends_with($_SERVER['SERVER_NAME'] ?? '', '.s-cm4all.cloud')) { | ||
| \add_filter('pre_site_transient_update_core', '\__return_null'); | ||
| } | ||
|
|
||
| \add_action( | ||
| hook_name: 'plugins_loaded', | ||
| callback: function () { | ||
| if (\get_option('impex_version') !== Impex::VERSION) { | ||
| Impex::getInstance()->__install(); | ||
| } | ||
|
|
||
| \do_action(Impex::WP_ACTION_REGISTER_PROVIDERS); | ||
| \do_action(Impex::WP_ACTION_REGISTER_PROFILES); | ||
| }, | ||
| ); | ||
|
|
||
| function enqueueClientAssets(bool $in_footer): string | ||
| { | ||
| static $CLIENT_ASSET_HANDLE; | ||
|
|
||
| if (!is_string($CLIENT_ASSET_HANDLE)) { | ||
| $CLIENT_ASSET_HANDLE = str_replace('\\', '-', __NAMESPACE__); | ||
| \add_action( | ||
| hook_name: Impex::WP_ACTION_ENQUEUE_IMPEX_PROVIDER_SCRIPT, | ||
| callback: function ($client_asset_handle, $in_footer) { | ||
| \cm4all\wp\impex\wp_enqueue_script( | ||
| handle: $client_asset_handle, | ||
| pluginRelativePath: 'dist/wp.impex.js', | ||
| in_footer: $in_footer | ||
| ); | ||
|
|
||
| $DEBUG_HANDLE = $client_asset_handle . '-debug'; | ||
| \cm4all\wp\impex\wp_enqueue_script( | ||
| handle: $DEBUG_HANDLE, | ||
| pluginRelativePath: 'dist/wp.impex.debug.js', | ||
| deps: [$client_asset_handle], | ||
| in_footer: $in_footer | ||
| ); | ||
|
|
||
| $STORE_HANDLE = $client_asset_handle . '-store'; | ||
| \cm4all\wp\impex\wp_enqueue_script( | ||
| handle: $STORE_HANDLE, | ||
| pluginRelativePath: 'dist/wp.impex.store.js', | ||
| deps: [$client_asset_handle, $DEBUG_HANDLE, 'wp-api-fetch', 'wp-data', 'wp-hooks'], | ||
| in_footer: $in_footer | ||
| ); | ||
|
|
||
| // prefetch initial impex data | ||
| $discoveryRequest = new \WP_REST_Request('GET', '/'); | ||
| $discoveryResponse = \rest_do_request($discoveryRequest); | ||
|
|
||
| $exportProfilesRequest = new \WP_REST_Request('GET', ImpexRestController::BASE_URI . ImpexExportProfileRESTController::REST_BASE); | ||
| $exportProfilesResponse = \rest_do_request($exportProfilesRequest); | ||
|
|
||
| $exportsRequest = new \WP_REST_Request('GET', ImpexRestController::BASE_URI . ImpexExportRESTController::REST_BASE); | ||
| $exportsResponse = \rest_do_request($exportsRequest); | ||
|
|
||
| $importProfilesRequest = new \WP_REST_Request('GET', ImpexRestController::BASE_URI . ImpexImportProfileRESTController::REST_BASE); | ||
| $importProfilesResponse = \rest_do_request($importProfilesRequest); | ||
|
|
||
| $importsRequest = new \WP_REST_Request('GET', ImpexRestController::BASE_URI . ImpexImportRESTController::REST_BASE); | ||
| $importsResponse = \rest_do_request($importsRequest); | ||
|
|
||
| $currentUserRequest = new \WP_REST_Request('GET', '/wp/v2/users/me'); | ||
| $currentUserResponse = \rest_do_request($currentUserRequest); | ||
|
|
||
| \wp_add_inline_script( | ||
| $STORE_HANDLE, | ||
| sprintf( | ||
| 'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );', | ||
| \wp_json_encode([ | ||
| $discoveryRequest->get_route() => [ | ||
| 'body' => $discoveryResponse->data, | ||
| 'headers' => $discoveryResponse->headers, | ||
| ], | ||
| $exportProfilesRequest->get_route() => [ | ||
| 'body' => $exportProfilesResponse->data, | ||
| 'headers' => $exportProfilesResponse->headers, | ||
| ], | ||
| $exportsRequest->get_route() => [ | ||
| 'body' => $exportsResponse->data, | ||
| 'headers' => $exportsResponse->headers, | ||
| ], | ||
| $importProfilesRequest->get_route() => [ | ||
| 'body' => $importProfilesResponse->data, | ||
| 'headers' => $importProfilesResponse->headers, | ||
| ], | ||
| $importsRequest->get_route() => [ | ||
| 'body' => $importsResponse->data, | ||
| 'headers' => $importsResponse->headers, | ||
| ], | ||
| $currentUserRequest->get_route() => [ | ||
| 'body' => $currentUserResponse->data, | ||
| 'headers' => $currentUserResponse->headers, | ||
| ], | ||
| ]) | ||
| ) | ||
| // add store initialization code | ||
| . sprintf("\nwp.impex.store.default(%s);", json_encode([ | ||
| 'namespace' => ImpexRestController::NAMESPACE, | ||
| 'base_uri' => ImpexRestController::BASE_URI, | ||
| 'site_url' => get_site_url(), | ||
| ])), | ||
| 'after' | ||
| ); | ||
|
|
||
| //echo (__('huhu !', 'cm4all-wp-impex')); | ||
| }, | ||
| accepted_args: 2, | ||
| ); | ||
|
|
||
| // register dummy style | ||
| wp_register_style( | ||
| handle: $CLIENT_ASSET_HANDLE, | ||
| pluginRelativePath: 'dist/wp.impex.css' | ||
| ); | ||
| } | ||
|
|
||
| \do_action(Impex::WP_ACTION_ENQUEUE_IMPEX_PROVIDER_SCRIPT, $CLIENT_ASSET_HANDLE, $in_footer); | ||
| \do_action(Impex::WP_ACTION_ENQUEUE_IMPEX_PROVIDER_STYLE, $CLIENT_ASSET_HANDLE); | ||
|
|
||
| return $CLIENT_ASSET_HANDLE; | ||
| } | ||
|
|
||
| \add_action( | ||
| hook_name: 'init', | ||
| callback: function () { | ||
| \load_plugin_textdomain(domain: 'cm4all-wp-impex', plugin_rel_path: basename(__DIR__) . '/languages/'); | ||
| }, | ||
| ); | ||
|
|
||
| \add_action(Impex::WP_ACTION_REGISTER_PROFILES, function () { | ||
| require_once __DIR__ . '/profiles/export-profile-base.php'; | ||
| require_once __DIR__ . '/profiles/import-profile-all.php'; | ||
| }, 0); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <?php | ||
|
|
||
| namespace cm4all\wp\impex\example; | ||
|
|
||
| use cm4all\wp\impex\AttachmentsExporter; | ||
| use cm4all\wp\impex\ContentExporter; | ||
| use cm4all\wp\impex\Impex; | ||
|
|
||
| // Exit if accessed directly. | ||
| if (!defined('ABSPATH')) { | ||
| exit(); | ||
| } | ||
|
|
||
| $profile = Impex::getInstance()->Export->addProfile('base'); | ||
| $profile->setDescription('Exports posts/pages including media assets'); | ||
| // export pages/posts/comments/block patterns/templates/template parts/reusable blocks | ||
| $profile->addTask('wordpress content', ContentExporter::PROVIDER_NAME, []); | ||
|
|
||
| // export uploads | ||
| $profile->addTask('wordpress attachments (uploads)', AttachmentsExporter::PROVIDER_NAME, []); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| <?php | ||
|
|
||
| namespace cm4all\wp\impex; | ||
|
|
||
| // Exit if accessed directly. | ||
| if (!defined('ABSPATH')) { | ||
| exit(); | ||
| } | ||
|
|
||
| $profile = Impex::getInstance()->Import->addProfile('all'); | ||
| $profile->setDescription('Import everything'); | ||
|
|
||
| // stupid php ... variable $provider needs to be defined before use statement in addProvider | ||
| $provider = null; | ||
|
|
||
| $provider = Impex::getInstance()->Import->addProvider('all', function (array $slice, array $options, ImpexImportTransformationContext $transformationContext) use (&$provider): bool { | ||
| foreach (Impex::getInstance()->Import->getProviders() as $_provider) { | ||
| if ($_provider === $provider) { | ||
| continue; | ||
| } | ||
|
|
||
| if (call_user_func($_provider->callback, $slice, $options, $transformationContext)) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| }); | ||
|
|
||
| $profile->addTask('main', $provider->name); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| === ${PLUGIN_NAME} === | ||
| Contributors: ${CONTRIBUTORS} | ||
| Plugin Name: ${PLUGIN_NAME} | ||
| Plugin URI: ${PLUGIN_URI} | ||
| Tags: ${TAGS} | ||
| Requires at least: ${REQUIRES_AT_LEAST} | ||
| Tested up to: ${REQUIRES_AT_LEAST} | ||
| Stable tag: ${VERSION} | ||
| Version: ${VERSION} | ||
| Requires PHP: ${REQUIRES_PHP} | ||
| License: GPLv2 or later | ||
| License URI: https://www.gnu.org/licenses/gpl-2.0.html | ||
| Author: ${AUTHOR} | ||
| Author URI: ${AUTHOR_URI} | ||
|
|
||
| ${DESCRIPTION} | ||
|
|
||
| == Description == | ||
|
|
||
| Impex is a Wordpress plugin that allows you to import and export data from your WordPress site. | ||
|
|
||
| The primary goal for Impex is to provide a **true Open Source Wordpress Plugin** for importing/exporting your Wordpress data (including data from third party plugins/themes). | ||
|
|
||
| **This plugin is in a early (but working) stage.** | ||
|
|
||
| Impex provides : | ||
|
|
||
| * Wordpress hooks for third party plugins to expose their own data to Impex Import / Export without being dependant to Impex. | ||
|
|
||
| * a (semi) streaming architecture for large data exports/imports. | ||
|
|
||
| * Definition of configurable import/export profiles defining data providers to use and configuration of these data providers | ||
|
|
||
| * A user interface for importing / exporting data via Impex WP admin screen | ||
|
|
||
| * the Impex API is designed to support resumable/abortable imports and exports. | ||
|
|
||
| * Impex provides also a REST API seamlessy integrated into Wordpress | ||
|
|
||
| * Impex is explicitly designed for use in managed Wordpress instances | ||
|
|
||
| * Impex development relies heavily on PHP unit testing its feature sset to be stable and consistent. | ||
|
|
||
| **Your help is welcome !!** | ||
|
|
||
| == Frequently Asked Questions == | ||
|
|
||
| = Why is that Plugin written in PHP 8 ? = | ||
|
|
||
| Because PHP 8 | ||
|
|
||
| * is much faster than any previous PHP Version | ||
|
|
||
| * allows a much cleaner PHP code | ||
|
|
||
| => We will investigate into transpiling the Plugin to PHP 7.4 in the future. | ||
|
|
||
| = Whats the current feature set ? = | ||
|
|
||
| * Impex provides right now generic providers for | ||
|
|
||
| * import/export Wordpress posts/pages **including attachments/uploads** and custom post types | ||
|
|
||
| * import/export configurable database tables (can be used export data from third party plugins/themes) | ||
|
|
||
| * import/export configurable wp_options (can be used export data from third party plugins/themes) | ||
|
|
||
| * Impex imports and exports data as plain files directly to your local filesystem so that you can operate on the exported data (and attachments) without hassle. | ||
|
|
||
| * Impex supports snapshots to allow you to rollback to a previous state of your data without downloading the whole data to your local machine | ||
|
|
||
| = Whats planned for the future ? = | ||
|
|
||
| * developer documentation integrating your plugin / theme into Impex | ||
|
|
||
| * a intuitive user interface allowing to configure import/export profiles and configuration of the used data providers based on React and JSON Schema | ||
|
|
||
| * user documentation | ||
|
|
||
| * monthly releases | ||
|
|
||
| == Screenshots == | ||
|
|
||
| 1. This screen shot description corresponds to screenshot-1.(png|jpg|jpeg|gif). Screenshots are stored in the /assets directory. | ||
| 2. This is the second screen shot | ||
|
|
||
| == Changelog == | ||
|
|
||
| ${CHANGELOG} | ||
|
|
||
| == Upgrade Notice == | ||
|
|
||
| There is currently no upgrade needed. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import components from "@wordpress/components"; | ||
| import { cancelCircleFilled } from "@wordpress/icons"; | ||
|
|
||
| import React from "React"; | ||
|
|
||
| export default function ({ title, doDelete, onRequestClose, children }) { | ||
| const onDelete = async () => { | ||
| await doDelete(); | ||
|
|
||
| onRequestClose(); | ||
| }; | ||
|
|
||
| return ( | ||
| <components.Modal | ||
| title={title} | ||
| icon={cancelCircleFilled} | ||
| onRequestClose={() => onRequestClose()} | ||
| > | ||
| <p>{children}</p> | ||
| <components.Button variant="primary" isDestructive onClick={onDelete}> | ||
| Delete | ||
| </components.Button> | ||
| </components.Modal> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,314 @@ | ||
| import element from "@wordpress/element"; | ||
| import components from "@wordpress/components"; | ||
| import apiFetch from "@wordpress/api-fetch"; | ||
| import hooks from "@wordpress/hooks"; | ||
| import url from "@wordpress/url"; | ||
| import data from "@wordpress/data"; | ||
| import { __, sprintf } from "@wordpress/i18n"; | ||
| import Debug from "@cm4all-impex/debug"; | ||
| import ImpexFilters from "@cm4all-impex/filters"; | ||
| import { edit, cancelCircleFilled, download } from "@wordpress/icons"; | ||
|
|
||
| import RenameModal from "./rename-modal.mjs"; | ||
| import DeleteModal from "./delete-modal.mjs"; | ||
| import useScreenContext from "./screen-context.mjs"; | ||
|
|
||
| import Store from "@cm4all-impex/store"; | ||
|
|
||
| const debug = Debug.default("wp.impex.dashboard.export"); | ||
| debug("loaded"); | ||
|
|
||
| //const { __, sprintf } = i18n; | ||
|
|
||
| export default function Export() { | ||
| const { settings, exportProfiles, exports } = data.useSelect((select) => { | ||
| const store = select(Store.KEY); | ||
| return { | ||
| settings: store.getSettings(), | ||
| exportProfiles: store.getExportProfiles(), | ||
| exports: store.getExports(), | ||
| }; | ||
| }); | ||
|
|
||
| const [exportProfile, setExportProfile] = element.useState(); | ||
| const exportProfileSelectRef = element.useRef(); | ||
|
|
||
| const _setExportProfile = (exportProfileName = null) => { | ||
| const exportProfile = exportProfiles.find( | ||
| (_) => _.name === exportProfileName | ||
| ); | ||
| setExportProfile(exportProfile); | ||
| exportProfileSelectRef.current.title = exportProfile?.description; | ||
| }; | ||
|
|
||
| element.useEffect(() => { | ||
| _setExportProfile(exportProfiles?.[0]?.name); | ||
| }, [exportProfiles]); | ||
|
|
||
| const { | ||
| createExport: _createExport, | ||
| updateExport, | ||
| deleteExport, | ||
| } = data.useDispatch(Store.KEY /*, []*/); | ||
|
|
||
| const [modal, setModal] = element.useState(null); | ||
| const [progress, setProgress] = element.useState(null); | ||
|
|
||
| const screenContext = useScreenContext(); | ||
|
|
||
| console.log({ exportProfile, exportProfiles }); | ||
| const { currentUser } = data.useSelect((select) => ({ | ||
| currentUser: select("core").getCurrentUser(), | ||
| })); | ||
|
|
||
| const createExport = async () => { | ||
| const site_url = new URL(settings["site_url"]); | ||
|
|
||
| const date = screenContext.currentDateString(); | ||
| const name = `${site_url.hostname}-${exportProfile.name}-${date}`; | ||
| const description = `Export '${exportProfile.name}' created by user '${currentUser.name}' at ${date}`; | ||
|
|
||
| setProgress({ | ||
| component: ( | ||
| <components.Modal | ||
| title={__("Creating snapshot", "cm4all-wp-impex")} | ||
| onRequestClose={() => {}} | ||
| overlayClassName="blocking" | ||
| > | ||
| <progress indeterminate="true"></progress> | ||
| </components.Modal> | ||
| ), | ||
| }); | ||
| await _createExport(exportProfile, name, description); | ||
| setProgress(); | ||
| }; | ||
|
|
||
| const _saveSlicesChunk = async (exportDirHandle, response, chunk) => { | ||
| const slices = await response; | ||
| debug(`_saveSlicesChunk(chunk=%o) : %o`, chunk, response); | ||
|
|
||
| // create chunk sub directory | ||
| const chunkDirHandle = await exportDirHandle.getDirectoryHandle( | ||
| `chunk-${chunk.toString().padStart(4, "0")}`, | ||
| { create: true } | ||
| ); | ||
|
|
||
| return Promise.all( | ||
| slices.map(async (slice, index) => { | ||
| const sliceFileHandle = await chunkDirHandle.getFileHandle( | ||
| `slice-${index.toString().padStart(4, "0")}.json`, | ||
| { create: true } | ||
| ); | ||
|
|
||
| slice = await hooks.applyFilters( | ||
| ImpexFilters.SLICE_REST_UNMARSHAL, | ||
| ImpexFilters.NAMESPACE, | ||
| slice, | ||
| index, | ||
| chunkDirHandle | ||
| ); | ||
|
|
||
| // Create a FileSystemWritableFileStream to write to. | ||
| const writable = await sliceFileHandle.createWritable(); | ||
| // Write the contents of the file to the stream. | ||
| await writable.write(JSON.stringify(slice, null, " ")); | ||
| debug("slice(=%o) = %o", index, slice); | ||
| // Close the file and write the contents to disk. | ||
| await writable.close(); | ||
| }) | ||
| ); | ||
| }; | ||
|
|
||
| const onDownloadExport = async (_export) => { | ||
| let _exportFolderName = null; | ||
| // showDirectoryPicker will throw a DOMExxception in case the user pressed cancel | ||
| try { | ||
| // colons need to be replaced otherwise showDirectoryPicker will fail | ||
| _exportFolderName = screenContext.normalizeFilename(_export.name); | ||
| } catch { | ||
| return; | ||
| } | ||
|
|
||
| // see https://web.dev/file-system-access/ | ||
| const exportsDirHandle = await window.showDirectoryPicker({ | ||
| // You can suggest a default start directory by passing a startIn property to the showSaveFilePicker | ||
| startIn: "downloads", | ||
| // If an id is specified, the file picker implementation will remember a separate last-used directory for pickers with that same id. | ||
| id: _exportFolderName, | ||
| }); | ||
|
|
||
| const exportDirHandle = await exportsDirHandle.getDirectoryHandle( | ||
| _exportFolderName, | ||
| { | ||
| create: true, | ||
| } | ||
| ); | ||
| debug("download export %o", _export); | ||
|
|
||
| const path = `${settings.base_uri}/export/${_export.id}/slice`; | ||
|
|
||
| setProgress({ | ||
| component: ( | ||
| <components.Modal | ||
| title={__("Downloading snapshot", "cm4all-wp-impex")} | ||
| onRequestClose={() => {}} | ||
| overlayClassName="blocking" | ||
| > | ||
| <progress indeterminate="true"></progress> | ||
| </components.Modal> | ||
| ), | ||
| }); | ||
|
|
||
| const initialResponse = await apiFetch({ | ||
| path, | ||
| // parse: false is needed to geta access to the headers | ||
| parse: false, | ||
| }); | ||
|
|
||
| const x_wp_total = Number.parseInt( | ||
| initialResponse.headers.get("X-WP-Total"), | ||
| 10 | ||
| ); | ||
| const x_wp_total_pages = Number.parseInt( | ||
| initialResponse.headers.get("X-WP-TotalPages") | ||
| ); | ||
|
|
||
| const sliceChunks = [ | ||
| _saveSlicesChunk(exportDirHandle, initialResponse.json(), 1), | ||
| ]; | ||
| for (let chunk = 2; chunk <= x_wp_total_pages; chunk++) { | ||
| sliceChunks.push( | ||
| _saveSlicesChunk( | ||
| exportDirHandle, | ||
| apiFetch({ | ||
| path: url.addQueryArgs(path, { page: chunk }), | ||
| }), | ||
| chunk | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| await Promise.all(sliceChunks); | ||
| setProgress(null); | ||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| <components.Panel | ||
| className="export" | ||
| header={__("Export", "cm4all-wp-impex")} | ||
| > | ||
| <components.PanelBody | ||
| title={__("Create snapshot", "cm4all-wp-impex")} | ||
| opened | ||
| className="create-export-form" | ||
| > | ||
| <wp.components.SelectControl | ||
| ref={exportProfileSelectRef} | ||
| disabled={!exportProfiles.length} | ||
| label={__("Export Profile", "cm4all-wp-impex")} | ||
| value={exportProfile?.name} | ||
| onChange={(exportProfileName) => | ||
| _setExportProfile(exportProfileName) | ||
| } | ||
| options={[ | ||
| { | ||
| name: __("Select an Export profile", "cm4all-wp-impex"), | ||
| disabled: true, | ||
| }, | ||
| ...exportProfiles, | ||
| ].map((_) => ({ | ||
| value: _.disabled ? undefined : _.name, | ||
| label: _.name, | ||
| disabled: _.disabled, | ||
| }))} | ||
| help={__( | ||
| "Export profiles define which Wordpress data should be extracted to the snapshot", | ||
| "cm4all-wp-impex" | ||
| )} | ||
| ></wp.components.SelectControl> | ||
|
|
||
| <components.Button | ||
| isPrimary | ||
| onClick={createExport} | ||
| disabled={ | ||
| !exportProfiles.find((_) => _.name === exportProfile?.name) | ||
| } | ||
| > | ||
| {__("Create Snapshot", "cm4all-wp-impex")} | ||
| </components.Button> | ||
| </components.PanelBody> | ||
| {exports.map((_, index) => ( | ||
| <components.PanelBody | ||
| key={_.id} | ||
| title={_.name} | ||
| initialOpen={index === 0} | ||
| > | ||
| <components.PanelRow> | ||
| <components.Button | ||
| isSecondary | ||
| onClick={() => onDownloadExport(_)} | ||
| icon={download} | ||
| > | ||
| {__("Download snapshot", "cm4all-wp-impex")} | ||
| </components.Button> | ||
| <components.DropdownMenu | ||
| // icon={moreVertical} | ||
| label={__( | ||
| "Additional actions on this export", | ||
| "cm4all-wp-impex" | ||
| )} | ||
| controls={[ | ||
| { | ||
| title: __("Edit", "cm4all-wp-impex"), | ||
| icon: edit, | ||
| onClick: () => | ||
| setModal({ | ||
| component: RenameModal, | ||
| props: { | ||
| title: __("Edit export", "cm4all-wp-impex"), | ||
| async doSave(data) { | ||
| await updateExport(_.id, data); | ||
| }, | ||
| item: _, | ||
| }, | ||
| }), | ||
| }, | ||
| { | ||
| title: __("Delete", "cm4all-wp-impex"), | ||
| icon: cancelCircleFilled, | ||
| onClick: () => | ||
| setModal({ | ||
| component: DeleteModal, | ||
| props: { | ||
| title: __("Delete export", "cm4all-wp-impex"), | ||
| children: ( | ||
| <> | ||
| {__("Are you really sure to delete export")} | ||
| <code>{_.name}</code>? | ||
| </> | ||
| ), | ||
| async doDelete() { | ||
| await deleteExport(_.id); | ||
| }, | ||
| }, | ||
| }), | ||
| }, | ||
| ]} | ||
| /> | ||
| </components.PanelRow> | ||
| <components.PanelRow> | ||
| <pre>{JSON.stringify(_, null, " ")}</pre> | ||
| </components.PanelRow> | ||
| </components.PanelBody> | ||
| ))} | ||
| </components.Panel> | ||
| {modal && <modal.component {...modal.props} onRequestClose={setModal} />} | ||
| {progress && ( | ||
| <components.Fill name="progress" onRequestClose={() => {}}> | ||
| {progress.component} | ||
| </components.Fill> | ||
| )} | ||
| </> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,312 @@ | ||
| import element from "@wordpress/element"; | ||
| import components from "@wordpress/components"; | ||
| import data from "@wordpress/data"; | ||
| import url from "@wordpress/url"; | ||
| import { __, sprintf } from "@wordpress/i18n"; | ||
| import hooks from "@wordpress/hooks"; | ||
| import ImpexFilters from "@cm4all-impex/filters"; | ||
| import Debug from "@cm4all-impex/debug"; | ||
| import apiFetch from "@wordpress/api-fetch"; | ||
| import { | ||
| edit, | ||
| cancelCircleFilled, | ||
| upload, | ||
| cloudUpload, | ||
| } from "@wordpress/icons"; | ||
| import RenameModal from "./rename-modal.mjs"; | ||
| import DeleteModal from "./delete-modal.mjs"; | ||
| import useScreenContext from "./screen-context.mjs"; | ||
|
|
||
| import Store from "@cm4all-impex/store"; | ||
|
|
||
| const debug = Debug.default("wp.impex.dashboard.import"); | ||
| debug("loaded"); | ||
|
|
||
| export default function Import() { | ||
| // @TODO: add dragn drop support for uploading an export ? | ||
| // https://medium.com/@650egor/simple-drag-and-drop-file-upload-in-react-2cb409d88929 | ||
| // https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/getAsFileSystemHandle | ||
| // https://wicg.github.io/file-system-access/#drag-and-drop | ||
|
|
||
| const { currentUser } = data.useSelect((select) => ({ | ||
| currentUser: select("core").getCurrentUser(), | ||
| })); | ||
|
|
||
| const { settings, importProfiles, imports } = data.useSelect((select) => { | ||
| const store = select(Store.KEY); | ||
| return { | ||
| settings: store.getSettings(), | ||
| importProfiles: store.getImportProfiles(), | ||
| imports: store.getImports(), | ||
| }; | ||
| }); | ||
|
|
||
| const { createImport, updateImport, deleteImport, consumeImport } = | ||
| data.useDispatch(Store.KEY /*, []*/); | ||
|
|
||
| const [modal, setModal] = element.useState(null); | ||
| const [progress, setProgress] = element.useState(null); | ||
|
|
||
| const screenContext = useScreenContext(); | ||
|
|
||
| const [importProfile, setImportProfile] = element.useState(); | ||
|
|
||
| const importProfileSelectRef = element.useRef(); | ||
|
|
||
| const _setImportProfile = (importProfileName = null) => { | ||
| const importProfile = importProfiles.find( | ||
| (_) => _.name === importProfileName | ||
| ); | ||
| setImportProfile(importProfile); | ||
| importProfileSelectRef.current.title = importProfile?.description; | ||
| }; | ||
|
|
||
| element.useEffect(() => { | ||
| _setImportProfile(importProfiles?.[0]?.name); | ||
| }, [importProfiles]); | ||
|
|
||
| const onConsumeImport = async (_import) => { | ||
| debug("onConsumeImport(%o)", _import); | ||
|
|
||
| setProgress({ | ||
| component: ( | ||
| <components.Modal | ||
| title={__("Importing data into Wordpress ...", "cm4all-wp-impex")} | ||
| onRequestClose={() => {}} | ||
| overlayClassName="blocking" | ||
| > | ||
| <progress indeterminate="true"></progress> | ||
| </components.Modal> | ||
| ), | ||
| }); | ||
|
|
||
| await consumeImport(_import.id, {}, null, null); | ||
| setProgress(); | ||
| }; | ||
|
|
||
| const _getSliceFiles = async (importDirHandle) => { | ||
| const slices = []; | ||
| for await (let sliceChunkDirectoryHandle of importDirHandle.values()) { | ||
| for await (let sliceFileHandle of sliceChunkDirectoryHandle.values()) { | ||
| if (sliceFileHandle.name.match(/^slice-\d+\.json$/)) { | ||
| slices.push({ | ||
| fileHandle: sliceFileHandle, | ||
| dirHandle: sliceChunkDirectoryHandle, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| slices.sort((l, r) => { | ||
| const cval = l.dirHandle.name.localeCompare(r.dirHandle.name); | ||
|
|
||
| if (cval === 0) { | ||
| return l.fileHandle.name.localeCompare(r.fileHandle.name); | ||
| } | ||
|
|
||
| return cval; | ||
| }); | ||
|
|
||
| return slices; | ||
| }; | ||
|
|
||
| const onUpload = async () => { | ||
| let importDirHandle = null; | ||
| // showDirectoryPicker will throw a DOMExxception in case the user pressed cancel | ||
| try { | ||
| // see https://web.dev/file-system-access/ | ||
| importDirHandle = await window.showDirectoryPicker({ | ||
| // You can suggest a default start directory by passing a startIn property to the showSaveFilePicker | ||
| startIn: "downloads", | ||
| }); | ||
| } catch { | ||
| return; | ||
| } | ||
|
|
||
| debug("upload export %o", importDirHandle.name); | ||
|
|
||
| const date = screenContext.currentDateString(); | ||
| const name = importDirHandle.name; | ||
| const description = `Import '${importDirHandle.name}' created by user '${currentUser.name}' at ${date}`; | ||
|
|
||
| setProgress({ | ||
| component: ( | ||
| <components.Modal | ||
| title={__("Uploading snapshot", "cm4all-wp-impex")} | ||
| onRequestClose={() => {}} | ||
| overlayClassName="blocking" | ||
| > | ||
| <progress indeterminate="true"></progress> | ||
| </components.Modal> | ||
| ), | ||
| }); | ||
|
|
||
| const _import = (await createImport(name, description, importProfile, [])) | ||
| .payload; | ||
|
|
||
| const sliceFiles = await _getSliceFiles(importDirHandle); | ||
|
|
||
| const path = `${settings.base_uri}/import/${_import.id}/slice`; | ||
|
|
||
| const sliceUploads = sliceFiles.map( | ||
| async ({ fileHandle, dirHandle }, position) => { | ||
| const formData = new FormData(); | ||
| let slice = JSON.parse(await (await fileHandle.getFile()).text()); | ||
|
|
||
| slice = await hooks.applyFilters( | ||
| ImpexFilters.SLICE_REST_UPLOAD, | ||
| ImpexFilters.NAMESPACE, | ||
| slice, | ||
| parseInt(fileHandle.name.match(/^slice-(\d+)\.json$/)[1]), | ||
| dirHandle, | ||
| formData | ||
| ); | ||
|
|
||
| if (slice) { | ||
| debug("upload %o", { | ||
| position, | ||
| file: fileHandle.name, | ||
| dir: dirHandle.name, | ||
| }); | ||
| formData.append("slice", JSON.stringify(slice, null, " ")); | ||
|
|
||
| return apiFetch({ | ||
| method: "POST", | ||
| path: url.addQueryArgs(path, { position }), | ||
| body: formData, | ||
|
|
||
| parse: false, | ||
| }); | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| const finished = await Promise.all(sliceUploads); | ||
| setProgress(); | ||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| <components.Panel | ||
| className="import" | ||
| header={__("Import", "cm4all-wp-impex")} | ||
| > | ||
| <components.PanelBody | ||
| title={__("Upload snapshot to Wordpress", "cm4all-wp-impex")} | ||
| opened | ||
| className="upload-import-form" | ||
| > | ||
| <wp.components.SelectControl | ||
| ref={importProfileSelectRef} | ||
| disabled={!importProfiles.length} | ||
| label={__("Import Profile", "cm4all-wp-impex")} | ||
| value={importProfile?.name} | ||
| onChange={(importProfileName) => | ||
| _setImportProfile(importProfileName) | ||
| } | ||
| options={[ | ||
| { | ||
| name: __("Select an Import profile", "cm4all-wp-impex"), | ||
| disabled: true, | ||
| }, | ||
| ...importProfiles, | ||
| ].map((_) => ({ | ||
| value: _.disabled ? undefined : _.name, | ||
| label: _.name, | ||
| disabled: _.disabled, | ||
| }))} | ||
| help={__( | ||
| "Import profiles define which Wordpress data should be consumed from the snapshot", | ||
| "cm4all-wp-impex" | ||
| )} | ||
| ></wp.components.SelectControl> | ||
| <components.Button | ||
| isPrimary | ||
| onClick={onUpload} | ||
| icon={upload} | ||
| disabled={!importProfile} | ||
| > | ||
| {__("Upload snapshot", "cm4all-wp-impex")} | ||
| </components.Button> | ||
| </components.PanelBody> | ||
| {imports.map((_, index) => ( | ||
| <components.PanelBody | ||
| key={_.id} | ||
| title={_.name} | ||
| initialOpen={index === 0} | ||
| > | ||
| <components.PanelRow> | ||
| <components.Button | ||
| isDestructive | ||
| isPrimary | ||
| onClick={() => onConsumeImport(_)} | ||
| icon={cloudUpload} | ||
| > | ||
| {__("Import uploaded snapshot", "cm4all-wp-impex")} | ||
| </components.Button> | ||
| <components.DropdownMenu | ||
| // icon={moreVertical} | ||
| label={__( | ||
| "Additional actions on this import", | ||
| "cm4all-wp-impex" | ||
| )} | ||
| controls={[ | ||
| { | ||
| title: __("Edit", "cm4all-wp-impex"), | ||
| icon: edit, | ||
| onClick: () => | ||
| setModal({ | ||
| component: RenameModal, | ||
| props: { | ||
| title: __("Edit import snapshot", "cm4all-wp-impex"), | ||
| async doSave(data) { | ||
| await updateImport(_.id, data); | ||
| }, | ||
| item: _, | ||
| }, | ||
| }), | ||
| }, | ||
| { | ||
| title: __("Delete", "cm4all-wp-impex"), | ||
| icon: cancelCircleFilled, | ||
| onClick: () => | ||
| setModal({ | ||
| component: DeleteModal, | ||
| props: { | ||
| title: __("Delete import", "cm4all-wp-impex"), | ||
| children: ( | ||
| <> | ||
| {__( | ||
| "Are you really sure to delete import", | ||
| "cm4all-wp-impex" | ||
| )} | ||
| <code>{_.name}</code>? | ||
| </> | ||
| ), | ||
| async doDelete() { | ||
| await deleteImport(_.id); | ||
| }, | ||
| }, | ||
| }), | ||
| }, | ||
| ]} | ||
| /> | ||
| </components.PanelRow> | ||
| <components.PanelRow> | ||
| <pre style={{ overflow: "auto" }}> | ||
| {JSON.stringify(_, null, " ")} | ||
| </pre> | ||
| </components.PanelRow> | ||
| </components.PanelBody> | ||
| ))} | ||
| </components.Panel> | ||
| {modal && <modal.component {...modal.props} onRequestClose={setModal} />} | ||
|
|
||
| {progress && ( | ||
| <components.Fill name="progress" onRequestClose={() => {}}> | ||
| {progress.component} | ||
| </components.Fill> | ||
| )} | ||
| </> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import element from "@wordpress/element"; | ||
| import components from "@wordpress/components"; | ||
| import data from "@wordpress/data"; | ||
| import { __, sprintf } from "@wordpress/i18n"; | ||
| import { edit } from "@wordpress/icons"; | ||
|
|
||
| export default function RenameModal({ title, doSave, item, onRequestClose }) { | ||
| const [name, setName] = element.useState(item.name); | ||
| const [description, setDescription] = element.useState(item.description); | ||
|
|
||
| const onSave = async () => { | ||
| await doSave({ name, description }); | ||
|
|
||
| onRequestClose(); | ||
| }; | ||
|
|
||
| return ( | ||
| <components.Modal | ||
| title={title} | ||
| icon={edit} | ||
| onRequestClose={() => onRequestClose()} | ||
| > | ||
| <components.TextControl | ||
| label={__("Name", "cm4all-wp-impex")} | ||
| help={__("Name should be short and human readable", "cm4all-wp-impex")} | ||
| value={name} | ||
| onChange={setName} | ||
| /> | ||
|
|
||
| <components.TextareaControl | ||
| label={__("Description", "cm4all-wp-impex")} | ||
| help={__( | ||
| "Description may contain more expressive information describing the item", | ||
| "cm4all-wp-impex", | ||
| )} | ||
| value={description} | ||
| onChange={setDescription} | ||
| /> | ||
|
|
||
| <components.Button | ||
| variant="primary" | ||
| onClick={onSave} | ||
| disabled={name === item.name && description === item.description} | ||
| > | ||
| Save | ||
| </components.Button> | ||
| </components.Modal> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import element from "@wordpress/element"; | ||
|
|
||
| const ContextProvider = element.createContext(); | ||
|
|
||
| export default function useScreenContext() { | ||
| return element.useContext(ContextProvider); | ||
| } | ||
|
|
||
| ScreenContext = { | ||
| normalizeFilename(fileName) { | ||
| return ( | ||
| fileName | ||
| .replace(/[^a-z0-9\-_]/gi, "_") | ||
| .replace(/(-+)|(_+)/gi, ($) => $[0]) | ||
| .toLowerCase() | ||
| // allow a maximum of 32 characters | ||
| .slice(-32) | ||
| ); | ||
| }, | ||
| currentDateString() { | ||
| const date = new Date(); | ||
| return `${date.getFullYear()}-${("0" + (date.getMonth() + 1)).slice(-2)}-${( | ||
| "0" + date.getDate() | ||
| ).slice(-2)}-${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`; | ||
| }, | ||
| }; | ||
|
|
||
| export function ScreenContextProvider({ children }) { | ||
| return ( | ||
| <ContextProvider.Provider value={ScreenContext}> | ||
| {children} | ||
| </ContextProvider.Provider> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import element from "@wordpress/element"; | ||
| import components from "@wordpress/components"; | ||
| import { __, sprintf } from "@wordpress/i18n"; | ||
| import Debug from "@cm4all-impex/debug"; | ||
|
|
||
| import Export from "./export.mjs"; | ||
| import Import from "./import.mjs"; | ||
|
|
||
| const debug = Debug.default("wp.impex.dashboard.screen"); | ||
| debug("loaded"); | ||
|
|
||
| const isFileystemApiAvailable = | ||
| typeof window.showDirectoryPicker === "function"; | ||
|
|
||
| export default function () { | ||
| return ( | ||
| <div> | ||
| <h1>{__("Impex", "cm4all-wp-impex")}</h1> | ||
|
|
||
| <components.SlotFillProvider> | ||
| <components.Flex direction="row" align="top"> | ||
| <components.FlexItem isBlock> | ||
| <Export /> | ||
| </components.FlexItem> | ||
|
|
||
| <components.FlexItem isBlock> | ||
| <Import /> | ||
| </components.FlexItem> | ||
| </components.Flex> | ||
|
|
||
| <components.Slot name="progress" /> | ||
|
|
||
| {!isFileystemApiAvailable && ( | ||
| <components.Modal | ||
| title="Ouch - your browser does not support the File System Access API :-(" | ||
| onRequestClose={() => {}} | ||
| > | ||
| <p> | ||
| Impex Import / Export requires a browser implementing the{" "} | ||
| <a href="https://web.dev/file-system-access/"> | ||
| File System Access API | ||
| </a> | ||
| . | ||
| </p> | ||
| <p> | ||
| Currently only Chromium based browsers like Chrome, Chromium, MS | ||
| Edge are known to support this feature. | ||
| </p> | ||
| <p> | ||
| See{" "} | ||
| <a href="https://caniuse.com/mdn-api_window_showdirectorypicker"> | ||
| here | ||
| </a>{" "} | ||
| to find the latest list of browsers supporting the{" "} | ||
| <a href="https://web.dev/file-system-access/"> | ||
| File System Access API | ||
| </a>{" "} | ||
| feature. | ||
| </p> | ||
| </components.Modal> | ||
| )} | ||
| </components.SlotFillProvider> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import element from "@wordpress/element"; | ||
| import Debug from "@cm4all-impex/debug"; | ||
|
|
||
| import Screen from "./components/screen.mjs"; | ||
| import { ScreenContextProvider } from "./components/screen-context.mjs"; | ||
|
|
||
| import "./wp.impex.dashboard.scss"; | ||
|
|
||
| const debug = Debug.default("wp.impex.dashboard"); | ||
| debug("loaded"); | ||
|
|
||
| // render impex dashboard only if not error notice (=> wordpress importer plugin is not installed) is shown | ||
| if (!document.querySelector(".notice.notice-error")) { | ||
| element.render( | ||
| <ScreenContextProvider> | ||
| <Screen /> | ||
| </ScreenContextProvider>, | ||
| document.getElementById("cm4all_wp_impex_wp_admin_dashboard") | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| .wrap { | ||
| //background-color: cyan; | ||
| } | ||
|
|
||
| button > svg { | ||
| width: 21px; | ||
| } | ||
|
|
||
| .components-modal__screen-overlay { | ||
| &.blocking { | ||
| cursor: wait; | ||
| } | ||
|
|
||
| PROGRESS { | ||
| width: 100%; | ||
| } | ||
|
|
||
| .components-text-control__input, | ||
| .components-textarea-control__input { | ||
| // fix input widths in rename export modal | ||
| width: calc(100% - 16px) !important; | ||
| } | ||
| } | ||
|
|
||
| .components-panel.export, | ||
| .components-panel.import { | ||
| margin-top: 12px; | ||
|
|
||
| .create-export-form, | ||
| .upload-import-form { | ||
| background-color: rgb(240, 240, 241); | ||
|
|
||
| .components-panel__body-title { | ||
| pointer-events: none; | ||
|
|
||
| .components-panel__body-toggle > * { | ||
| visibility: hidden; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// background-color: rgb(240, 240, 241); | ||
| /// | ||
| /// | ||
|
|
||
| #cm4all_wp_impex_wp_admin_dashboard { | ||
| /* | ||
| background-color: yellow; | ||
| color: blue; | ||
| .components-button { | ||
| background: green !important; | ||
| &.is-primary { | ||
| background: red; | ||
| } | ||
| } | ||
| button.components-button.is-primary { | ||
| background-color: silver !important; | ||
| } | ||
| */ | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| const Debug = require("debug"); | ||
|
|
||
| export default Debug; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import hooks from "@wordpress/hooks"; | ||
| import Debug from "@cm4all-impex/debug"; | ||
| import ImpexFilters from "@cm4all-impex/filters"; | ||
|
|
||
| const debug = Debug.default("wp.impex.attachments"); | ||
| debug("huhu!"); | ||
|
|
||
| hooks.addFilter( | ||
| ImpexFilters.SLICE_REST_UNMARSHAL, | ||
| ImpexFilters.NAMESPACE, | ||
| async function (namespace, slice, sliceIndex, chunkDirHandle) { | ||
| if ( | ||
| slice["tag"] === "attachment" && | ||
| slice["meta"]["entity"] === "attachment" && | ||
| slice["type"] === "resource" | ||
| ) { | ||
| const _links_self = slice["_links"]?.["self"]; | ||
|
|
||
| if (_links_self) { | ||
| // download attachments to local folder | ||
| for (const entry of _links_self) { | ||
| const href = entry["href"]; | ||
|
|
||
| let path = href.split(/[\\/]/); | ||
|
|
||
| const filename = | ||
| `slice-${sliceIndex.toString().padStart(4, "0")}-` + path.pop(); | ||
| //path.push(filename); | ||
| //path = path.join("//"); | ||
|
|
||
| await fetch(href).then(async (response) => { | ||
| attachmentFileHandle = await chunkDirHandle.getFileHandle( | ||
| filename, | ||
| { | ||
| create: true, | ||
| }, | ||
| ); | ||
| const writable = await attachmentFileHandle.createWritable(); | ||
|
|
||
| await response.body.pipeTo(writable); | ||
|
|
||
| // see https://web.dev/file-system-access/ | ||
| // => pipeTo() closes the destination pipe by default, no need to close it. | ||
| // await writable.close(); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| delete slice["_links"]; | ||
| /* | ||
| slice['_links']['self'][] = [ | ||
| 'href' => slice[Impex::SLICE_META]['data']['guid'], | ||
| 'tag' => AttachmentsExporter::SLICE_TAG, | ||
| 'provider' => AttachmentsExporter::PROVIDER_NAME, | ||
| ]; | ||
| */ | ||
| } | ||
|
|
||
| return slice; | ||
| }, | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import hooks from "@wordpress/hooks"; | ||
| import Debug from "@cm4all-impex/debug"; | ||
| import ImpexFilters from "@cm4all-impex/filters"; | ||
|
|
||
| const debug = Debug.default("wp.impex.attachments"); | ||
| debug("huhu!"); | ||
|
|
||
| hooks.addFilter( | ||
| ImpexFilters.SLICE_REST_UPLOAD, | ||
| ImpexFilters.NAMESPACE, | ||
| async function (namespace, slice, sliceIndex, chunkDirHandle, formData) { | ||
| if ( | ||
| slice["tag"] === "attachment" && | ||
| slice["meta"]["entity"] === "attachment" && | ||
| slice["type"] === "resource" | ||
| ) { | ||
| const localAttachmentFilename = | ||
| `slice-${sliceIndex.toString().padStart(4, "0")}-` + | ||
| slice["data"].split(/[\\/]/).pop(); | ||
|
|
||
| const localAttachmentFileHandle = await chunkDirHandle | ||
| .getFileHandle(localAttachmentFilename) | ||
| .catch((e) => { | ||
| console.log(localAttachmentFilename); | ||
| return Promise.reject(e); | ||
| }); | ||
|
|
||
| formData.append( | ||
| // constant was injected using \wp_add_inline_script | ||
| wp.impex.extension.import.attachment | ||
| .WP_FILTER_IMPORT_REST_SLICE_UPLOAD_FILE, | ||
| await localAttachmentFileHandle.getFile(), | ||
| ); | ||
| } | ||
|
|
||
| return slice; | ||
| }, | ||
| ); | ||
|
|
||
| export default {}; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| /* | ||
| this acts a initial dependency and is right now empty | ||
| */ | ||
|
|
||
| import "./wp.impex.scss"; | ||
|
|
||
| const NAMESPACE = "cm4all-impex"; | ||
|
|
||
| const filters = { | ||
| SLICE_REST_UNMARSHAL: "slice_rest_unmarshal", | ||
| SLICE_REST_UPLOAD: "slice_rest_upload", | ||
| NAMESPACE, | ||
| }; | ||
|
|
||
| const actions = { | ||
| NAMESPACE, | ||
| }; | ||
|
|
||
| export { filters, actions }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| /* | ||
| this acts a initial dependency and is right now empty | ||
| */ |