PluginProbe
Code Snippets / 3.10.0
Code Snippets v3.10.0
3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 3.0.1 All 64 releases
code-snippets / php / Migration / Export / Download_Code.php

Download_Code.php in Code Snippets 3.10.0, at php/Migration/Export/Download_Code.php

254 lines 6.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Code_Snippets\Migration\Export;
4
5 use Code_Snippets\Model\Snippet;
6 use WP_Error;
7 use WP_Filesystem_Direct;
8 use ZipArchive;
9 use function sanitize_title;
10 use function wp_json_encode;
11 use function wp_delete_file;
12 use function wp_tempnam;
13
14 /**
15 * Builds downloadable snippet code files.
16 *
17 * @package Code_Snippets
18 */
19 class Download_Code {
20
21 /**
22 * Bulk download archive filename.
23 */
24 private const ARCHIVE_FILENAME_TEMPLATE = 'code-snippets-%d.zip';
25
26 /**
27 * Content types keyed by snippet type.
28 *
29 * @var array<string, string>
30 */
31 private const CONTENT_TYPES = [
32 'php' => 'text/php',
33 'html' => 'text/html',
34 'css' => 'text/css',
35 'js' => 'text/javascript',
36 'cond' => 'application/json',
37 ];
38
39 /**
40 * File extensions keyed by snippet type.
41 *
42 * @var array<string, string>
43 */
44 private const FILE_EXTENSIONS = [
45 'php' => 'php',
46 'html' => 'html',
47 'css' => 'css',
48 'js' => 'js',
49 'cond' => 'json',
50 ];
51
52 /**
53 * Build a single downloadable code file.
54 *
55 * @param Snippet $snippet Snippet to export.
56 *
57 * @return array{filename:string, content_type:string, content:string}
58 */
59 public static function build_snippet_download( Snippet $snippet ): array {
60 $export = new Export_Code( [ $snippet->id ], $snippet->network );
61
62 return [
63 'filename' => self::build_filename( $snippet ),
64 'content_type' => self::CONTENT_TYPES[ $snippet->type ] ?? 'application/octet-stream',
65 'content' => self::build_content( $snippet, $export ),
66 ];
67 }
68
69 /**
70 * Build a ZIP archive containing multiple snippet downloads.
71 *
72 * @param Snippet[] $snippets Snippets to export.
73 *
74 * @return array{filename:string, content_type:string, content:string}|WP_Error
75 */
76 public static function build_archive_download( array $snippets ) {
77 $archive_filename = sprintf( self::ARCHIVE_FILENAME_TEMPLATE, time() );
78
79 if ( ! class_exists( ZipArchive::class ) ) {
80 return new WP_Error(
81 'zip_archive_unavailable',
82 __( 'Multiple snippet downloads require the ZipArchive PHP extension.', 'code-snippets' ),
83 [ 'status' => 501 ]
84 );
85 }
86
87 $temp_file = wp_tempnam( $archive_filename );
88
89 if ( ! is_string( $temp_file ) ) {
90 return new WP_Error(
91 'zip_archive_temp_file_failed',
92 __( 'The temporary download archive could not be created.', 'code-snippets' ),
93 [ 'status' => 500 ]
94 );
95 }
96
97 $zip = new ZipArchive();
98 $open_result = $zip->open( $temp_file, ZipArchive::CREATE | ZipArchive::OVERWRITE );
99
100 if ( true !== $open_result ) {
101 wp_delete_file( $temp_file );
102
103 return new WP_Error(
104 'zip_archive_open_failed',
105 __( 'The snippet download archive could not be created.', 'code-snippets' ),
106 [ 'status' => 500 ]
107 );
108 }
109
110 $used_filenames = [];
111
112 foreach ( $snippets as $snippet ) {
113 $download = self::build_snippet_download( $snippet );
114 $filename = self::get_unique_filename( $download['filename'], $snippet, $used_filenames );
115
116 $zip->addFromString( $filename, $download['content'] );
117 $used_filenames[ $filename ] = true;
118 }
119
120 $zip->close();
121
122 // WP_Filesystem_Direct is used intentionally here: the temp file was just created by
123 // wp_tempnam() on the local server filesystem, so the request filesystem context
124 // (FTP, SSH) is irrelevant. Using direct access avoids WP Filesystem bootstrapping
125 // overhead for a file we created and will immediately delete.
126 $filesystem = new WP_Filesystem_Direct( null );
127 $content = $filesystem->get_contents( $temp_file );
128 wp_delete_file( $temp_file );
129
130 if ( ! is_string( $content ) ) {
131 return new WP_Error(
132 'zip_archive_read_failed',
133 __( 'The snippet download archive could not be read.', 'code-snippets' ),
134 [ 'status' => 500 ]
135 );
136 }
137
138 return [
139 'filename' => $archive_filename,
140 'content_type' => 'application/zip',
141 'content' => $content,
142 ];
143 }
144
145 /**
146 * Build the raw file content for a snippet download.
147 *
148 * @param Snippet $snippet Snippet being exported.
149 * @param Export_Code $export Export helper for PHP-aware formatting.
150 *
151 * @return string
152 */
153 private static function build_content( Snippet $snippet, Export_Code $export ): string {
154 switch ( $snippet->type ) {
155 case 'html':
156 case 'css':
157 case 'js':
158 return self::normalize_content( $snippet->code );
159
160 case 'cond':
161 $decoded = json_decode( $snippet->code, true );
162
163 return JSON_ERROR_NONE === json_last_error()
164 ? self::normalize_content( wp_json_encode( $decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) )
165 : self::normalize_content( $snippet->code );
166
167 case 'php':
168 default:
169 return self::normalize_php_content( $export->generate_export() );
170 }
171 }
172
173 /**
174 * Build a deterministic filename for a snippet download.
175 *
176 * @param Snippet $snippet Snippet to export.
177 *
178 * @return string
179 */
180 private static function build_filename( Snippet $snippet ): string {
181 $title = sanitize_title( $snippet->name );
182
183 if ( '' === $title ) {
184 $title = "snippet-$snippet->id";
185 }
186
187 $extension = self::FILE_EXTENSIONS[ $snippet->type ] ?? 'txt';
188
189 return "$title.code-snippets.$extension";
190 }
191
192 /**
193 * Normalize snippet content with a trailing newline.
194 *
195 * @param string $content Snippet content.
196 *
197 * @return string
198 */
199 private static function normalize_content( string $content ): string {
200 $content = rtrim( $content );
201
202 return '' === $content ? '' : "$content\n";
203 }
204
205 /**
206 * Normalize PHP content to include an opening tag and trailing newline.
207 *
208 * @param string $content Snippet content.
209 *
210 * @return string
211 */
212 private static function normalize_php_content( string $content ): string {
213 $content = ltrim( $content );
214
215 if ( 0 !== strpos( $content, '<?php' ) ) {
216 $content = "<?php\n\n" . ltrim( $content );
217 }
218
219 return self::normalize_content( $content );
220 }
221
222 /**
223 * Avoid duplicate filenames within the same archive.
224 *
225 * @param string $filename Proposed filename.
226 * @param Snippet $snippet Snippet being exported.
227 * @param array<string,bool> $used_filenames Archive filenames already in use.
228 *
229 * @return string
230 */
231 private static function get_unique_filename( string $filename, Snippet $snippet, array $used_filenames ): string {
232 if ( ! isset( $used_filenames[ $filename ] ) ) {
233 return $filename;
234 }
235
236 $extension = self::FILE_EXTENSIONS[ $snippet->type ] ?? 'txt';
237 $title = sanitize_title( $snippet->name );
238
239 if ( '' === $title ) {
240 $title = 'snippet';
241 }
242
243 $counter = 2;
244 $unique_filename = sprintf( '%s-%d.code-snippets.%s', $title, $snippet->id, $extension );
245
246 while ( isset( $used_filenames[ $unique_filename ] ) ) {
247 $unique_filename = sprintf( '%s-%d-%d.code-snippets.%s', $title, $snippet->id, $counter, $extension );
248 ++$counter;
249 }
250
251 return $unique_filename;
252 }
253 }
254