PluginProbe
Blocks – Reusable Content, Shortcodes & Site Variables / trunk
Blocks – Reusable Content, Shortcodes & Site Variables vtrunk
26.09.03.15 26.08.31.21 26.08.22.20 26.08.22.22 26.08.22.17 26.08.22.13 26.08.21.19 26.08.07.23 26.07.19.14 26.07.13.21 26.07.13.17 26.07.12.13 026.07.07.21 026.07.05.18 026.06.26.20 026.06.26.21 026.06.08.20 026.05.13.14 026.04.29.10 trunk 026.02.22.22 026.03.16.23 026.04.23.13
blocks / src / Admin / TransferPage.php

TransferPage.php in Blocks – Reusable Content, Shortcodes & Site Variables trunk, at src/Admin/TransferPage.php

172 lines 5.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace RenzoJohnson\Blocks\Admin;
6
7 use JsonException;
8 use RenzoJohnson\Blocks\Transfer\TransferService;
9
10 \defined( 'ABSPATH' ) || exit;
11
12 final class TransferPage {
13
14 private const PAGE_SLUG = 'blocks-tools';
15 private const MAX_UPLOAD_BYTES = 5_242_880;
16
17 public function __construct( private readonly TransferService $service, private readonly TransferView $view ) {
18 }
19
20 public function register_hooks(): void {
21 \add_action( 'admin_menu', $this->add_settings_page( ... ), 22 );
22 \add_action( 'admin_post_blocks_export', $this->handle_export( ... ) );
23 \add_action( 'admin_post_blocks_import', $this->handle_import( ... ) );
24 }
25
26 public function add_settings_page(): void {
27 \add_submenu_page(
28 'block',
29 \__( 'Import / Export', 'blocks' ),
30 \__( 'Import / Export', 'blocks' ),
31 'manage_options',
32 self::PAGE_SLUG,
33 $this->view->render( ... ),
34 );
35 }
36
37 public function handle_export(): never {
38 $this->authorize( 'blocks_export', 'blocks_export_nonce' );
39
40 $json = \wp_json_encode( $this->service->export_payload(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
41
42 if ( ! \is_string( $json ) ) {
43 \wp_die( \esc_html__( 'Export failed.', 'blocks' ) );
44 }
45
46 $host = \wp_parse_url( \home_url(), PHP_URL_HOST );
47 $host = \is_string( $host ) ? $host : 'site';
48 $filename = \sanitize_file_name( 'blocks-export-' . $host . '-' . \gmdate( 'Ymd-His' ) . '.json' );
49
50 \nocache_headers();
51 \header( 'Content-Type: application/json; charset=utf-8' );
52 \header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
53 echo $json; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON attachment encoded by wp_json_encode().
54 exit;
55 }
56
57 public function handle_import(): never {
58 $this->authorize( 'blocks_import', 'blocks_import_nonce' );
59
60 $file = $this->uploaded_file();
61
62 if ( \is_wp_error( $file ) ) {
63 $this->redirect_with( array( 'import_error' => 'nofile' ) );
64 }
65
66 $result = $this->process_upload( $file );
67
68 if ( \is_wp_error( $result ) ) {
69 $this->redirect_with( array( 'import_error' => 'invalid' ) );
70 }
71
72 $this->redirect_with(
73 array(
74 'imported' => 1,
75 'created' => $result['created'],
76 'updated' => $result['updated'],
77 'skipped' => $result['skipped'],
78 ),
79 );
80 }
81
82 /**
83 * @param array<string, mixed> $file
84 * @return array{created: int, updated: int, skipped: int, variables: int}|\WP_Error
85 */
86 private function process_upload( array $file ): array|\WP_Error {
87
88 if ( ! \function_exists( 'wp_handle_upload' ) ) {
89 require_once ABSPATH . 'wp-admin/includes/file.php';
90 }
91
92 $upload = \wp_handle_upload(
93 $file,
94 array(
95 'test_form' => false,
96 'mimes' => array( 'json' => 'application/json' ),
97 ),
98 );
99
100 if ( isset( $upload['error'] ) || ! isset( $upload['file'] ) || ! \is_string( $upload['file'] ) ) {
101 return new \WP_Error( 'blocks_import_upload' );
102 }
103
104 $path = $upload['file'];
105
106 try {
107 $size = \filesize( $path );
108
109 if ( ! \is_int( $size ) || 0 >= $size || self::MAX_UPLOAD_BYTES < $size ) {
110 return new \WP_Error( 'blocks_import_size' );
111 }
112
113 $raw = \file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local upload managed by wp_handle_upload().
114
115 if ( ! \is_string( $raw ) ) {
116 return new \WP_Error( 'blocks_import_read' );
117 }
118
119 $payload = \json_decode( $raw, true, 512, JSON_THROW_ON_ERROR );
120
121 if ( ! \is_array( $payload ) ) {
122 return new \WP_Error( 'blocks_import_invalid' );
123 }
124
125 // phpcs:disable WordPress.Security.NonceVerification.Missing -- handle_import() verifies the import nonce before calling this private method.
126 $result = $this->service->import_payload( $payload, isset( $_POST['blocks_import_overwrite'] ) );
127 // phpcs:enable
128
129 return $result;
130 } catch ( JsonException ) {
131 return new \WP_Error( 'blocks_import_json' );
132 } finally {
133 if ( \is_file( $path ) ) {
134 \wp_delete_file( $path );
135 }
136 }
137 }
138
139 private function authorize( string $action, string $nonce_name ): void {
140 if ( ! \current_user_can( 'manage_options' ) ) {
141 \wp_die( \esc_html__( 'Permission denied.', 'blocks' ) );
142 }
143
144 \check_admin_referer( $action, $nonce_name );
145 }
146
147 /** @return array<string, mixed>|\WP_Error */
148 private function uploaded_file(): array|\WP_Error {
149 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- authorize() verified the nonce; WordPress validates and moves the upload.
150 $file = $_FILES['blocks_import_file'] ?? null;
151
152 if ( ! \is_array( $file ) ) {
153 return new \WP_Error( 'blocks_import_no_file' );
154 }
155
156 $error = $file['error'] ?? UPLOAD_ERR_NO_FILE;
157 $size = $file['size'] ?? 0;
158
159 if ( UPLOAD_ERR_OK !== $error || ! \is_int( $size ) || 0 >= $size || self::MAX_UPLOAD_BYTES < $size ) {
160 return new \WP_Error( 'blocks_import_no_file' );
161 }
162
163 return $file;
164 }
165
166 /** @param array<string, int|string> $parameters */
167 private function redirect_with( array $parameters ): never {
168 \wp_safe_redirect( \add_query_arg( $parameters, \admin_url( 'admin.php?page=' . self::PAGE_SLUG ) ) );
169 exit;
170 }
171 }
172