kit = $kit; $this->settings = $settings; $this->kits = $kits; } public function register(): void { // After Admin::menu() (priority 10) so the "Zadex" parent exists. add_action('admin_menu', [$this, 'menu'], 20); add_action('admin_post_' . self::EXPORT, [$this, 'handle_export']); add_action('admin_post_' . self::IMPORT, [$this, 'handle_import']); add_action('admin_post_' . self::LIBRARY_URL, [$this, 'handle_library_url']); } public function menu(): void { add_submenu_page('zadex', __('Import / Export', 'zadex'), __('Import / Export', 'zadex'), 'manage_options', self::PAGE, [$this, 'render']); } public static function page_url(array $args = []): string { return add_query_arg(array_merge(['page' => self::PAGE], $args), admin_url('admin.php')); } public static function max_upload(): int { $max = (int) apply_filters('zadex/kit/max_upload', self::MAX_UPLOAD); if (function_exists('wp_max_upload_size')) { $max = min($max, (int) wp_max_upload_size()); } return max(1, $max); } private function require_capability(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('You are not allowed to do this.', 'zadex'), '', ['response' => 403]); } } public function handle_export(): void { $this->require_capability(); check_admin_referer(self::EXPORT); $ids = isset($_POST['pages']) && is_array($_POST['pages']) ? array_filter(array_map('absint', wp_unslash($_POST['pages']))) : []; $library = !empty($_POST['library']); $global = !empty($_POST['global']); $tmp = function_exists('wp_tempnam') ? (string) wp_tempnam('zadex-kit.zip') : (string) tempnam(sys_get_temp_dir(), 'zxk'); $result = $this->kit->export_zip($tmp, array_values($ids), $library, $global); if (is_wp_error($result)) { wp_delete_file($tmp); wp_die(esc_html($result->get_error_message())); } $name = 'zadex-kit-' . gmdate('Y-m-d') . '.zip'; // Anything already buffered (notices printed by other code in debug mode) would corrupt the zip. while (ob_get_level() > 0) { ob_end_clean(); } nocache_headers(); header('Content-Type: application/zip'); header('Content-Disposition: attachment; filename="' . $name . '"'); header('Content-Length: ' . (string) filesize($tmp)); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- streams our own temp zip to the browser; WP_Filesystem has no streaming read. readfile($tmp); wp_delete_file($tmp); exit; } /** * Validates an uploaded kit (`$_FILES['kit']`). * * @return string|\WP_Error path of the uploaded temp file */ public static function validate_upload($file) { if (!is_array($file) || !isset($file['tmp_name'], $file['name'], $file['error'])) { return new \WP_Error('zadex_no_file', __('Choose a kit file (.zip) to import.', 'zadex')); } if ((int) $file['error'] !== 0) { return new \WP_Error('zadex_upload_error', __('The upload failed. The file may be larger than the server allows.', 'zadex')); } if (strtolower(pathinfo((string) $file['name'], PATHINFO_EXTENSION)) !== 'zip') { return new \WP_Error('zadex_bad_type', __('A site kit is a .zip file.', 'zadex')); } $size = (int) ($file['size'] ?? 0); if ($size <= 0 || $size > self::max_upload()) { return new \WP_Error('zadex_too_large', __('The kit file is empty or too large.', 'zadex')); } $tmp = (string) $file['tmp_name']; if (!is_file($tmp) || (function_exists('is_uploaded_file') && !is_uploaded_file($tmp) && !apply_filters('zadex/kit/allow_local_file', false, $tmp))) { return new \WP_Error('zadex_no_file', __('Choose a kit file (.zip) to import.', 'zadex')); } // Only the 4-byte zip signature of the PHP upload temp file is read; WP_Filesystem has no partial read. $fh = fopen($tmp, 'rb'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- reads the zip magic bytes of the upload temp file. $magic = $fh ? (string) fread($fh, 4) : ''; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- see above. if ($fh) { fclose($fh); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- see above. } if ($magic !== "PK\x03\x04") { return new \WP_Error('zadex_bad_type', __('The file is not a valid zip archive.', 'zadex')); } return $tmp; } public function handle_import(): void { $this->require_capability(); check_admin_referer(self::IMPORT); $options = [ 'status' => isset($_POST['status']) && sanitize_key(wp_unslash($_POST['status'])) === 'publish' ? 'publish' : 'draft', 'global' => !empty($_POST['global']), 'library' => !empty($_POST['library']), ]; $path = self::validate_upload(UploadedFile::from_request('kit', self::IMPORT)); $report = null; if (is_wp_error($path)) { $report = ['error' => $path->get_error_message()]; } else { $result = $this->kit->import_zip($path, $options); $report = is_wp_error($result) ? ['error' => $result->get_error_message()] : $result; } if (is_string($path) && is_file($path)) { wp_delete_file($path); } set_transient(self::REPORT . get_current_user_id(), $report, 600); wp_safe_redirect(self::page_url()); exit; } public function handle_library_url(): void { $this->require_capability(); check_admin_referer(self::LIBRARY_URL); $url = isset($_POST['library_url']) ? trim(esc_url_raw(wp_unslash($_POST['library_url']))) : ''; $this->kits->flush_cache(); $this->settings->update(['library_url' => preg_match('#^https?://#i', $url) ? $url : '']); $this->kits->flush_cache(); Notices::add(self::PAGE, __('Template library source saved.', 'zadex')); wp_safe_redirect(self::page_url()); exit; } public function render(): void { if (!current_user_can('manage_options')) { return; } $pages = $this->kit->exportable_pages(); // The import report is stored per user by handle_import() and shown once, on this screen only. $report = get_transient(self::REPORT . get_current_user_id()); if ($report !== false) { delete_transient(self::REPORT . get_current_user_id()); } echo '
' . esc_html__('Downloads a .zip with the selected Zadex pages, your saved templates, the site settings (colours, fonts, breakpoints) and every image they use.', 'zadex') . '
'; echo ''; wp_register_script('zadex-kit-export', false, [], ZADEX_VERSION, true); wp_add_inline_script('zadex-kit-export', 'document.querySelectorAll("[data-zadex-check]").forEach(function(b){b.addEventListener("click",function(){var on=b.getAttribute("data-zadex-check")==="all";document.querySelectorAll("#zadex-kit-export input[name=\'pages[]\']").forEach(function(c){c.checked=on;});});});'); wp_enqueue_script('zadex-kit-export'); // Import. echo '' . esc_html(sprintf(__('Creates the kit\'s pages, adds its templates and images to this site and applies its site settings. Maximum file size: %s.', 'zadex'), size_format(self::max_upload()))) . '
'; echo ''; // Remote library. echo '' . esc_html__('URL of a kits.json index for the editor\'s template library. Leave empty to use the starter kits bundled with Zadex. Remote results are cached for 12 hours.', 'zadex') . '
'; echo ''; echo '' . esc_html((string) $report['error']) . '
' . esc_html__('Kit imported.', 'zadex') . ' '; echo esc_html(sprintf( /* translators: 1: pages, 2: templates, 3: media files */ __('%1$d pages, %2$d templates and %3$d media files were created.', 'zadex'), count($pages), (int) ($report['library'] ?? 0), (int) ($report['media'] ?? 0) )); if (!empty($report['global'])) { echo ' ' . esc_html__('Site settings were applied.', 'zadex'); } echo '
'; if ($pages) { echo '' . esc_html((string) $err) . '
'; } echo '