1 <?php |
1 <?php |
2 /** |
2 /** |
|
3 * Filesystem API: Top-level functionality |
|
4 * |
3 * Functions for reading, writing, modifying, and deleting files on the file system. |
5 * Functions for reading, writing, modifying, and deleting files on the file system. |
4 * Includes functionality for theme-specific files as well as operations for uploading, |
6 * Includes functionality for theme-specific files as well as operations for uploading, |
5 * archiving, and rendering output when necessary. |
7 * archiving, and rendering output when necessary. |
6 * |
8 * |
7 * @package WordPress |
9 * @package WordPress |
8 * @subpackage Administration |
10 * @subpackage Filesystem |
|
11 * @since 2.3.0 |
9 */ |
12 */ |
10 |
13 |
11 /** The descriptions for theme files. */ |
14 /** The descriptions for theme files. */ |
12 $wp_file_descriptions = array( |
15 $wp_file_descriptions = array( |
13 'index.php' => __( 'Main Index Template' ), |
16 'functions.php' => __( 'Theme Functions' ), |
14 'style.css' => __( 'Stylesheet' ), |
17 'header.php' => __( 'Theme Header' ), |
15 'editor-style.css' => __( 'Visual Editor Stylesheet' ), |
18 'footer.php' => __( 'Theme Footer' ), |
16 'editor-style-rtl.css' => __( 'Visual Editor RTL Stylesheet' ), |
19 'sidebar.php' => __( 'Sidebar' ), |
17 'rtl.css' => __( 'RTL Stylesheet' ), |
20 'comments.php' => __( 'Comments' ), |
18 'comments.php' => __( 'Comments' ), |
21 'searchform.php' => __( 'Search Form' ), |
19 'comments-popup.php' => __( 'Popup Comments' ), |
22 '404.php' => __( '404 Template' ), |
20 'footer.php' => __( 'Footer' ), |
23 'link.php' => __( 'Links Template' ), |
21 'header.php' => __( 'Header' ), |
24 // Archives |
22 'sidebar.php' => __( 'Sidebar' ), |
25 'index.php' => __( 'Main Index Template' ), |
23 'archive.php' => __( 'Archives' ), |
26 'archive.php' => __( 'Archives' ), |
24 'author.php' => __( 'Author Template' ), |
27 'author.php' => __( 'Author Template' ), |
25 'tag.php' => __( 'Tag Template' ), |
28 'taxonomy.php' => __( 'Taxonomy Template' ), |
26 'category.php' => __( 'Category Template' ), |
29 'category.php' => __( 'Category Template' ), |
27 'page.php' => __( 'Page Template' ), |
30 'tag.php' => __( 'Tag Template' ), |
28 'search.php' => __( 'Search Results' ), |
31 'home.php' => __( 'Posts Page' ), |
29 'searchform.php' => __( 'Search Form' ), |
32 'search.php' => __( 'Search Results' ), |
30 'single.php' => __( 'Single Post' ), |
33 'date.php' => __( 'Date Template' ), |
31 '404.php' => __( '404 Template' ), |
34 // Content |
32 'link.php' => __( 'Links Template' ), |
35 'singular.php' => __( 'Singular Template' ), |
33 'functions.php' => __( 'Theme Functions' ), |
36 'single.php' => __( 'Single Post' ), |
34 'attachment.php' => __( 'Attachment Template' ), |
37 'page.php' => __( 'Single Page' ), |
35 'image.php' => __('Image Attachment Template'), |
38 'front-page.php' => __( 'Homepage' ), |
36 'video.php' => __('Video Attachment Template'), |
39 // Attachments |
37 'audio.php' => __('Audio Attachment Template'), |
40 'attachment.php' => __( 'Attachment Template' ), |
38 'application.php' => __('Application Attachment Template'), |
41 'image.php' => __( 'Image Attachment Template' ), |
39 'my-hacks.php' => __( 'my-hacks.php (legacy hacks support)' ), |
42 'video.php' => __( 'Video Attachment Template' ), |
40 '.htaccess' => __( '.htaccess (for rewrite rules )' ), |
43 'audio.php' => __( 'Audio Attachment Template' ), |
|
44 'application.php' => __( 'Application Attachment Template' ), |
|
45 // Embeds |
|
46 'embed.php' => __( 'Embed Template' ), |
|
47 'embed-404.php' => __( 'Embed 404 Template' ), |
|
48 'embed-content.php' => __( 'Embed Content Template' ), |
|
49 'header-embed.php' => __( 'Embed Header Template' ), |
|
50 'footer-embed.php' => __( 'Embed Footer Template' ), |
|
51 // Stylesheets |
|
52 'style.css' => __( 'Stylesheet' ), |
|
53 'editor-style.css' => __( 'Visual Editor Stylesheet' ), |
|
54 'editor-style-rtl.css' => __( 'Visual Editor RTL Stylesheet' ), |
|
55 'rtl.css' => __( 'RTL Stylesheet' ), |
|
56 // Other |
|
57 'my-hacks.php' => __( 'my-hacks.php (legacy hacks support)' ), |
|
58 '.htaccess' => __( '.htaccess (for rewrite rules )' ), |
41 // Deprecated files |
59 // Deprecated files |
42 'wp-layout.css' => __( 'Stylesheet' ), |
60 'wp-layout.css' => __( 'Stylesheet' ), |
43 'wp-comments.php' => __( 'Comments Template' ), |
61 'wp-comments.php' => __( 'Comments Template' ), |
44 'wp-comments-popup.php' => __( 'Popup Comments Template' ), |
62 'wp-comments-popup.php' => __( 'Popup Comments Template' ), |
|
63 'comments-popup.php' => __( 'Popup Comments' ), |
45 ); |
64 ); |
46 |
65 |
47 /** |
66 /** |
48 * Get the description for standard WordPress theme files and other various standard |
67 * Get the description for standard WordPress theme files and other various standard |
49 * WordPress files |
68 * WordPress files |
50 * |
69 * |
51 * @since 1.5.0 |
70 * @since 1.5.0 |
52 * |
71 * |
53 * @uses $wp_file_descriptions |
72 * @global array $wp_file_descriptions Theme file descriptions. |
|
73 * @global array $allowed_files List of allowed files. |
54 * @param string $file Filesystem path or filename |
74 * @param string $file Filesystem path or filename |
55 * @return string Description of file from $wp_file_descriptions or basename of $file if description doesn't exist |
75 * @return string Description of file from $wp_file_descriptions or basename of $file if description doesn't exist. |
|
76 * Appends 'Page Template' to basename of $file if the file is a page template |
56 */ |
77 */ |
57 function get_file_description( $file ) { |
78 function get_file_description( $file ) { |
58 global $wp_file_descriptions; |
79 global $wp_file_descriptions, $allowed_files; |
59 |
80 |
60 if ( isset( $wp_file_descriptions[basename( $file )] ) ) { |
81 $dirname = pathinfo( $file, PATHINFO_DIRNAME ); |
61 return $wp_file_descriptions[basename( $file )]; |
82 |
62 } |
83 $file_path = $allowed_files[ $file ]; |
63 elseif ( file_exists( $file ) && is_file( $file ) ) { |
84 if ( isset( $wp_file_descriptions[ basename( $file ) ] ) && '.' === $dirname ) { |
64 $template_data = implode( '', file( $file ) ); |
85 return $wp_file_descriptions[ basename( $file ) ]; |
65 if ( preg_match( '|Template Name:(.*)$|mi', $template_data, $name )) |
86 } elseif ( file_exists( $file_path ) && is_file( $file_path ) ) { |
66 return sprintf( __( '%s Page Template' ), _cleanup_header_comment($name[1]) ); |
87 $template_data = implode( '', file( $file_path ) ); |
|
88 if ( preg_match( '|Template Name:(.*)$|mi', $template_data, $name ) ) { |
|
89 return sprintf( __( '%s Page Template' ), _cleanup_header_comment( $name[1] ) ); |
|
90 } |
67 } |
91 } |
68 |
92 |
69 return trim( basename( $file ) ); |
93 return trim( basename( $file ) ); |
70 } |
94 } |
71 |
95 |
94 /** |
118 /** |
95 * Returns a listing of all files in the specified folder and all subdirectories up to 100 levels deep. |
119 * Returns a listing of all files in the specified folder and all subdirectories up to 100 levels deep. |
96 * The depth of the recursiveness can be controlled by the $levels param. |
120 * The depth of the recursiveness can be controlled by the $levels param. |
97 * |
121 * |
98 * @since 2.6.0 |
122 * @since 2.6.0 |
|
123 * @since 4.9.0 Added the `$exclusions` parameter. |
99 * |
124 * |
100 * @param string $folder Optional. Full path to folder. Default empty. |
125 * @param string $folder Optional. Full path to folder. Default empty. |
101 * @param int $levels Optional. Levels of folders to follow, Default 100 (PHP Loop limit). |
126 * @param int $levels Optional. Levels of folders to follow, Default 100 (PHP Loop limit). |
|
127 * @param array $exclusions Optional. List of folders and files to skip. |
102 * @return bool|array False on failure, Else array of files |
128 * @return bool|array False on failure, Else array of files |
103 */ |
129 */ |
104 function list_files( $folder = '', $levels = 100 ) { |
130 function list_files( $folder = '', $levels = 100, $exclusions = array() ) { |
105 if ( empty($folder) ) |
131 if ( empty( $folder ) ) { |
106 return false; |
132 return false; |
107 |
133 } |
108 if ( ! $levels ) |
134 |
|
135 $folder = trailingslashit( $folder ); |
|
136 |
|
137 if ( ! $levels ) { |
109 return false; |
138 return false; |
|
139 } |
110 |
140 |
111 $files = array(); |
141 $files = array(); |
112 if ( $dir = @opendir( $folder ) ) { |
142 |
113 while (($file = readdir( $dir ) ) !== false ) { |
143 $dir = @opendir( $folder ); |
114 if ( in_array($file, array('.', '..') ) ) |
144 if ( $dir ) { |
|
145 while ( ( $file = readdir( $dir ) ) !== false ) { |
|
146 // Skip current and parent folder links. |
|
147 if ( in_array( $file, array( '.', '..' ), true ) ) { |
115 continue; |
148 continue; |
116 if ( is_dir( $folder . '/' . $file ) ) { |
149 } |
117 $files2 = list_files( $folder . '/' . $file, $levels - 1); |
150 |
118 if ( $files2 ) |
151 // Skip hidden and excluded files. |
|
152 if ( '.' === $file[0] || in_array( $file, $exclusions, true ) ) { |
|
153 continue; |
|
154 } |
|
155 |
|
156 if ( is_dir( $folder . $file ) ) { |
|
157 $files2 = list_files( $folder . $file, $levels - 1 ); |
|
158 if ( $files2 ) { |
119 $files = array_merge($files, $files2 ); |
159 $files = array_merge($files, $files2 ); |
120 else |
160 } else { |
121 $files[] = $folder . '/' . $file . '/'; |
161 $files[] = $folder . $file . '/'; |
|
162 } |
122 } else { |
163 } else { |
123 $files[] = $folder . '/' . $file; |
164 $files[] = $folder . $file; |
124 } |
165 } |
125 } |
166 } |
126 } |
167 } |
127 @closedir( $dir ); |
168 @closedir( $dir ); |
|
169 |
128 return $files; |
170 return $files; |
129 } |
171 } |
|
172 |
|
173 /** |
|
174 * Get list of file extensions that are editable in plugins. |
|
175 * |
|
176 * @since 4.9.0 |
|
177 * |
|
178 * @param string $plugin Plugin. |
|
179 * @return array File extensions. |
|
180 */ |
|
181 function wp_get_plugin_file_editable_extensions( $plugin ) { |
|
182 |
|
183 $editable_extensions = array( |
|
184 'bash', |
|
185 'conf', |
|
186 'css', |
|
187 'diff', |
|
188 'htm', |
|
189 'html', |
|
190 'http', |
|
191 'inc', |
|
192 'include', |
|
193 'js', |
|
194 'json', |
|
195 'jsx', |
|
196 'less', |
|
197 'md', |
|
198 'patch', |
|
199 'php', |
|
200 'php3', |
|
201 'php4', |
|
202 'php5', |
|
203 'php7', |
|
204 'phps', |
|
205 'phtml', |
|
206 'sass', |
|
207 'scss', |
|
208 'sh', |
|
209 'sql', |
|
210 'svg', |
|
211 'text', |
|
212 'txt', |
|
213 'xml', |
|
214 'yaml', |
|
215 'yml', |
|
216 ); |
|
217 |
|
218 /** |
|
219 * Filters file type extensions editable in the plugin editor. |
|
220 * |
|
221 * @since 2.8.0 |
|
222 * @since 4.9.0 Adds $plugin param. |
|
223 * |
|
224 * @param string $plugin Plugin file. |
|
225 * @param array $editable_extensions An array of editable plugin file extensions. |
|
226 */ |
|
227 $editable_extensions = (array) apply_filters( 'editable_extensions', $editable_extensions, $plugin ); |
|
228 |
|
229 return $editable_extensions; |
|
230 } |
|
231 |
|
232 /** |
|
233 * Get list of file extensions that are editable for a given theme. |
|
234 * |
|
235 * @param WP_Theme $theme Theme. |
|
236 * @return array File extensions. |
|
237 */ |
|
238 function wp_get_theme_file_editable_extensions( $theme ) { |
|
239 |
|
240 $default_types = array( |
|
241 'bash', |
|
242 'conf', |
|
243 'css', |
|
244 'diff', |
|
245 'htm', |
|
246 'html', |
|
247 'http', |
|
248 'inc', |
|
249 'include', |
|
250 'js', |
|
251 'json', |
|
252 'jsx', |
|
253 'less', |
|
254 'md', |
|
255 'patch', |
|
256 'php', |
|
257 'php3', |
|
258 'php4', |
|
259 'php5', |
|
260 'php7', |
|
261 'phps', |
|
262 'phtml', |
|
263 'sass', |
|
264 'scss', |
|
265 'sh', |
|
266 'sql', |
|
267 'svg', |
|
268 'text', |
|
269 'txt', |
|
270 'xml', |
|
271 'yaml', |
|
272 'yml', |
|
273 ); |
|
274 |
|
275 /** |
|
276 * Filters the list of file types allowed for editing in the Theme editor. |
|
277 * |
|
278 * @since 4.4.0 |
|
279 * |
|
280 * @param array $default_types List of file types. Default types include 'php' and 'css'. |
|
281 * @param WP_Theme $theme The current Theme object. |
|
282 */ |
|
283 $file_types = apply_filters( 'wp_theme_editor_filetypes', $default_types, $theme ); |
|
284 |
|
285 // Ensure that default types are still there. |
|
286 return array_unique( array_merge( $file_types, $default_types ) ); |
|
287 } |
|
288 |
|
289 /** |
|
290 * Print file editor templates (for plugins and themes). |
|
291 * |
|
292 * @since 4.9.0 |
|
293 */ |
|
294 function wp_print_file_editor_templates() { |
|
295 ?> |
|
296 <script type="text/html" id="tmpl-wp-file-editor-notice"> |
|
297 <div class="notice inline notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }} {{ data.classes || '' }}"> |
|
298 <# if ( 'php_error' === data.code ) { #> |
|
299 <p> |
|
300 <?php |
|
301 printf( |
|
302 /* translators: %$1s is line number and %1$s is file path. */ |
|
303 __( 'Your PHP code changes were rolled back due to an error on line %1$s of file %2$s. Please fix and try saving again.' ), |
|
304 '{{ data.line }}', |
|
305 '{{ data.file }}' |
|
306 ); |
|
307 ?> |
|
308 </p> |
|
309 <pre>{{ data.message }}</pre> |
|
310 <# } else if ( 'file_not_writable' === data.code ) { #> |
|
311 <p><?php _e( 'You need to make this file writable before you can save your changes. See <a href="https://codex.wordpress.org/Changing_File_Permissions">the Codex</a> for more information.' ); ?></p> |
|
312 <# } else { #> |
|
313 <p>{{ data.message || data.code }}</p> |
|
314 |
|
315 <# if ( 'lint_errors' === data.code ) { #> |
|
316 <p> |
|
317 <# var elementId = 'el-' + String( Math.random() ); #> |
|
318 <input id="{{ elementId }}" type="checkbox"> |
|
319 <label for="{{ elementId }}"><?php _e( 'Update anyway, even though it might break your site?' ); ?></label> |
|
320 </p> |
|
321 <# } #> |
|
322 <# } #> |
|
323 <# if ( data.dismissible ) { #> |
|
324 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php _e( 'Dismiss' ); ?></span></button> |
|
325 <# } #> |
|
326 </div> |
|
327 </script> |
|
328 <?php |
|
329 } |
|
330 |
|
331 /** |
|
332 * Attempt to edit a file for a theme or plugin. |
|
333 * |
|
334 * When editing a PHP file, loopback requests will be made to the admin and the homepage |
|
335 * to attempt to see if there is a fatal error introduced. If so, the PHP change will be |
|
336 * reverted. |
|
337 * |
|
338 * @since 4.9.0 |
|
339 * |
|
340 * @param array $args { |
|
341 * Args. Note that all of the arg values are already unslashed. They are, however, |
|
342 * coming straight from $_POST and are not validated or sanitized in any way. |
|
343 * |
|
344 * @type string $file Relative path to file. |
|
345 * @type string $plugin Plugin being edited. |
|
346 * @type string $theme Theme being edited. |
|
347 * @type string $newcontent New content for the file. |
|
348 * @type string $nonce Nonce. |
|
349 * } |
|
350 * @return true|WP_Error True on success or `WP_Error` on failure. |
|
351 */ |
|
352 function wp_edit_theme_plugin_file( $args ) { |
|
353 if ( empty( $args['file'] ) ) { |
|
354 return new WP_Error( 'missing_file' ); |
|
355 } |
|
356 $file = $args['file']; |
|
357 if ( 0 !== validate_file( $file ) ) { |
|
358 return new WP_Error( 'bad_file' ); |
|
359 } |
|
360 |
|
361 if ( ! isset( $args['newcontent'] ) ) { |
|
362 return new WP_Error( 'missing_content' ); |
|
363 } |
|
364 $content = $args['newcontent']; |
|
365 |
|
366 if ( ! isset( $args['nonce'] ) ) { |
|
367 return new WP_Error( 'missing_nonce' ); |
|
368 } |
|
369 |
|
370 $plugin = null; |
|
371 $theme = null; |
|
372 $real_file = null; |
|
373 if ( ! empty( $args['plugin'] ) ) { |
|
374 $plugin = $args['plugin']; |
|
375 |
|
376 if ( ! current_user_can( 'edit_plugins' ) ) { |
|
377 return new WP_Error( 'unauthorized', __( 'Sorry, you are not allowed to edit plugins for this site.' ) ); |
|
378 } |
|
379 |
|
380 if ( ! wp_verify_nonce( $args['nonce'], 'edit-plugin_' . $file ) ) { |
|
381 return new WP_Error( 'nonce_failure' ); |
|
382 } |
|
383 |
|
384 if ( ! array_key_exists( $plugin, get_plugins() ) ) { |
|
385 return new WP_Error( 'invalid_plugin' ); |
|
386 } |
|
387 |
|
388 if ( 0 !== validate_file( $file, get_plugin_files( $plugin ) ) ) { |
|
389 return new WP_Error( 'bad_plugin_file_path', __( 'Sorry, that file cannot be edited.' ) ); |
|
390 } |
|
391 |
|
392 $editable_extensions = wp_get_plugin_file_editable_extensions( $plugin ); |
|
393 |
|
394 $real_file = WP_PLUGIN_DIR . '/' . $file; |
|
395 |
|
396 $is_active = in_array( |
|
397 $plugin, |
|
398 (array) get_option( 'active_plugins', array() ), |
|
399 true |
|
400 ); |
|
401 |
|
402 } elseif ( ! empty( $args['theme'] ) ) { |
|
403 $stylesheet = $args['theme']; |
|
404 if ( 0 !== validate_file( $stylesheet ) ) { |
|
405 return new WP_Error( 'bad_theme_path' ); |
|
406 } |
|
407 |
|
408 if ( ! current_user_can( 'edit_themes' ) ) { |
|
409 return new WP_Error( 'unauthorized', __( 'Sorry, you are not allowed to edit templates for this site.' ) ); |
|
410 } |
|
411 |
|
412 $theme = wp_get_theme( $stylesheet ); |
|
413 if ( ! $theme->exists() ) { |
|
414 return new WP_Error( 'non_existent_theme', __( 'The requested theme does not exist.' ) ); |
|
415 } |
|
416 |
|
417 $real_file = $theme->get_stylesheet_directory() . '/' . $file; |
|
418 if ( ! wp_verify_nonce( $args['nonce'], 'edit-theme_' . $real_file . $stylesheet ) ) { |
|
419 return new WP_Error( 'nonce_failure' ); |
|
420 } |
|
421 |
|
422 if ( $theme->errors() && 'theme_no_stylesheet' === $theme->errors()->get_error_code() ) { |
|
423 return new WP_Error( |
|
424 'theme_no_stylesheet', |
|
425 __( 'The requested theme does not exist.' ) . ' ' . $theme->errors()->get_error_message() |
|
426 ); |
|
427 } |
|
428 |
|
429 $editable_extensions = wp_get_theme_file_editable_extensions( $theme ); |
|
430 |
|
431 $allowed_files = array(); |
|
432 foreach ( $editable_extensions as $type ) { |
|
433 switch ( $type ) { |
|
434 case 'php': |
|
435 $allowed_files = array_merge( $allowed_files, $theme->get_files( 'php', -1 ) ); |
|
436 break; |
|
437 case 'css': |
|
438 $style_files = $theme->get_files( 'css', -1 ); |
|
439 $allowed_files['style.css'] = $style_files['style.css']; |
|
440 $allowed_files = array_merge( $allowed_files, $style_files ); |
|
441 break; |
|
442 default: |
|
443 $allowed_files = array_merge( $allowed_files, $theme->get_files( $type, -1 ) ); |
|
444 break; |
|
445 } |
|
446 } |
|
447 |
|
448 // Compare based on relative paths |
|
449 if ( 0 !== validate_file( $file, array_keys( $allowed_files ) ) ) { |
|
450 return new WP_Error( 'disallowed_theme_file', __( 'Sorry, that file cannot be edited.' ) ); |
|
451 } |
|
452 |
|
453 $is_active = ( get_stylesheet() === $stylesheet || get_template() === $stylesheet ); |
|
454 } else { |
|
455 return new WP_Error( 'missing_theme_or_plugin' ); |
|
456 } |
|
457 |
|
458 // Ensure file is real. |
|
459 if ( ! is_file( $real_file ) ) { |
|
460 return new WP_Error( 'file_does_not_exist', __( 'No such file exists! Double check the name and try again.' ) ); |
|
461 } |
|
462 |
|
463 // Ensure file extension is allowed. |
|
464 $extension = null; |
|
465 if ( preg_match( '/\.([^.]+)$/', $real_file, $matches ) ) { |
|
466 $extension = strtolower( $matches[1] ); |
|
467 if ( ! in_array( $extension, $editable_extensions, true ) ) { |
|
468 return new WP_Error( 'illegal_file_type', __( 'Files of this type are not editable.' ) ); |
|
469 } |
|
470 } |
|
471 |
|
472 $previous_content = file_get_contents( $real_file ); |
|
473 |
|
474 if ( ! is_writeable( $real_file ) ) { |
|
475 return new WP_Error( 'file_not_writable' ); |
|
476 } |
|
477 |
|
478 $f = fopen( $real_file, 'w+' ); |
|
479 if ( false === $f ) { |
|
480 return new WP_Error( 'file_not_writable' ); |
|
481 } |
|
482 |
|
483 $written = fwrite( $f, $content ); |
|
484 fclose( $f ); |
|
485 if ( false === $written ) { |
|
486 return new WP_Error( 'unable_to_write', __( 'Unable to write to file.' ) ); |
|
487 } |
|
488 if ( 'php' === $extension && function_exists( 'opcache_invalidate' ) ) { |
|
489 opcache_invalidate( $real_file, true ); |
|
490 } |
|
491 |
|
492 if ( $is_active && 'php' === $extension ) { |
|
493 |
|
494 $scrape_key = md5( rand() ); |
|
495 $transient = 'scrape_key_' . $scrape_key; |
|
496 $scrape_nonce = strval( rand() ); |
|
497 set_transient( $transient, $scrape_nonce, 60 ); // It shouldn't take more than 60 seconds to make the two loopback requests. |
|
498 |
|
499 $cookies = wp_unslash( $_COOKIE ); |
|
500 $scrape_params = array( |
|
501 'wp_scrape_key' => $scrape_key, |
|
502 'wp_scrape_nonce' => $scrape_nonce, |
|
503 ); |
|
504 $headers = array( |
|
505 'Cache-Control' => 'no-cache', |
|
506 ); |
|
507 |
|
508 // Include Basic auth in loopback requests. |
|
509 if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) { |
|
510 $headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) ); |
|
511 } |
|
512 |
|
513 // Make sure PHP process doesn't die before loopback requests complete. |
|
514 @set_time_limit( 300 ); |
|
515 |
|
516 // Time to wait for loopback requests to finish. |
|
517 $timeout = 100; |
|
518 |
|
519 $needle_start = "###### wp_scraping_result_start:$scrape_key ######"; |
|
520 $needle_end = "###### wp_scraping_result_end:$scrape_key ######"; |
|
521 |
|
522 // Attempt loopback request to editor to see if user just whitescreened themselves. |
|
523 if ( $plugin ) { |
|
524 $url = add_query_arg( compact( 'plugin', 'file' ), admin_url( 'plugin-editor.php' ) ); |
|
525 } elseif ( isset( $stylesheet ) ) { |
|
526 $url = add_query_arg( |
|
527 array( |
|
528 'theme' => $stylesheet, |
|
529 'file' => $file, |
|
530 ), |
|
531 admin_url( 'theme-editor.php' ) |
|
532 ); |
|
533 } else { |
|
534 $url = admin_url(); |
|
535 } |
|
536 $url = add_query_arg( $scrape_params, $url ); |
|
537 $r = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout' ) ); |
|
538 $body = wp_remote_retrieve_body( $r ); |
|
539 $scrape_result_position = strpos( $body, $needle_start ); |
|
540 |
|
541 $loopback_request_failure = array( |
|
542 'code' => 'loopback_request_failed', |
|
543 'message' => __( 'Unable to communicate back with site to check for fatal errors, so the PHP change was reverted. You will need to upload your PHP file change by some other means, such as by using SFTP.' ), |
|
544 ); |
|
545 $json_parse_failure = array( |
|
546 'code' => 'json_parse_error', |
|
547 ); |
|
548 |
|
549 $result = null; |
|
550 if ( false === $scrape_result_position ) { |
|
551 $result = $loopback_request_failure; |
|
552 } else { |
|
553 $error_output = substr( $body, $scrape_result_position + strlen( $needle_start ) ); |
|
554 $error_output = substr( $error_output, 0, strpos( $error_output, $needle_end ) ); |
|
555 $result = json_decode( trim( $error_output ), true ); |
|
556 if ( empty( $result ) ) { |
|
557 $result = $json_parse_failure; |
|
558 } |
|
559 } |
|
560 |
|
561 // Try making request to homepage as well to see if visitors have been whitescreened. |
|
562 if ( true === $result ) { |
|
563 $url = home_url( '/' ); |
|
564 $url = add_query_arg( $scrape_params, $url ); |
|
565 $r = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout' ) ); |
|
566 $body = wp_remote_retrieve_body( $r ); |
|
567 $scrape_result_position = strpos( $body, $needle_start ); |
|
568 |
|
569 if ( false === $scrape_result_position ) { |
|
570 $result = $loopback_request_failure; |
|
571 } else { |
|
572 $error_output = substr( $body, $scrape_result_position + strlen( $needle_start ) ); |
|
573 $error_output = substr( $error_output, 0, strpos( $error_output, $needle_end ) ); |
|
574 $result = json_decode( trim( $error_output ), true ); |
|
575 if ( empty( $result ) ) { |
|
576 $result = $json_parse_failure; |
|
577 } |
|
578 } |
|
579 } |
|
580 |
|
581 delete_transient( $transient ); |
|
582 |
|
583 if ( true !== $result ) { |
|
584 |
|
585 // Roll-back file change. |
|
586 file_put_contents( $real_file, $previous_content ); |
|
587 if ( function_exists( 'opcache_invalidate' ) ) { |
|
588 opcache_invalidate( $real_file, true ); |
|
589 } |
|
590 |
|
591 if ( ! isset( $result['message'] ) ) { |
|
592 $message = __( 'Something went wrong.' ); |
|
593 } else { |
|
594 $message = $result['message']; |
|
595 unset( $result['message'] ); |
|
596 } |
|
597 return new WP_Error( 'php_error', $message, $result ); |
|
598 } |
|
599 } |
|
600 |
|
601 if ( $theme instanceof WP_Theme ) { |
|
602 $theme->cache_delete(); |
|
603 } |
|
604 |
|
605 return true; |
|
606 } |
|
607 |
130 |
608 |
131 /** |
609 /** |
132 * Returns a filename of a Temporary unique file. |
610 * Returns a filename of a Temporary unique file. |
133 * Please note that the calling function must unlink() this itself. |
611 * Please note that the calling function must unlink() this itself. |
134 * |
612 * |
972 |
1514 |
973 /** |
1515 /** |
974 * Displays a form to the user to request for their FTP/SSH details in order |
1516 * Displays a form to the user to request for their FTP/SSH details in order |
975 * to connect to the filesystem. |
1517 * to connect to the filesystem. |
976 * |
1518 * |
977 * All chosen/entered details are saved, Excluding the Password. |
1519 * All chosen/entered details are saved, excluding the password. |
978 * |
1520 * |
979 * Hostnames may be in the form of hostname:portnumber (eg: wordpress.org:2467) |
1521 * Hostnames may be in the form of hostname:portnumber (eg: wordpress.org:2467) |
980 * to specify an alternate FTP/SSH port. |
1522 * to specify an alternate FTP/SSH port. |
981 * |
1523 * |
982 * Plugins may override this form by returning true|false via the |
1524 * Plugins may override this form by returning true|false via the {@see 'request_filesystem_credentials'} filter. |
983 * {@see 'request_filesystem_credentials'} filter. |
1525 * |
984 * |
1526 * @since 2.5.0 |
985 * @since 2.5. |
1527 * @since 4.6.0 The `$context` parameter default changed from `false` to an empty string. |
986 * |
1528 * |
987 * @todo Properly mark optional arguments as such |
1529 * @global string $pagenow |
988 * |
1530 * |
989 * @param string $form_post the URL to post the form to |
1531 * @param string $form_post The URL to post the form to. |
990 * @param string $type the chosen Filesystem method in use |
1532 * @param string $type Optional. Chosen type of filesystem. Default empty. |
991 * @param boolean $error if the current request has failed to connect |
1533 * @param bool $error Optional. Whether the current request has failed to connect. |
992 * @param string $context The directory which is needed access to, The write-test will be performed on this directory by get_filesystem_method() |
1534 * Default false. |
993 * @param array $extra_fields Extra POST fields which should be checked for to be included in the post. |
1535 * @param string $context Optional. Full path to the directory that is tested for being |
994 * @param bool $allow_relaxed_file_ownership Whether to allow Group/World writable. |
1536 * writable. Default empty. |
995 * @return boolean False on failure. True on success. |
1537 * @param array $extra_fields Optional. Extra `POST` fields to be checked for inclusion in |
996 */ |
1538 * the post. Default null. |
997 function request_filesystem_credentials($form_post, $type = '', $error = false, $context = false, $extra_fields = null, $allow_relaxed_file_ownership = false ) { |
1539 * @param bool $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable. Default false. |
|
1540 * |
|
1541 * @return bool False on failure, true on success. |
|
1542 */ |
|
1543 function request_filesystem_credentials( $form_post, $type = '', $error = false, $context = '', $extra_fields = null, $allow_relaxed_file_ownership = false ) { |
|
1544 global $pagenow; |
998 |
1545 |
999 /** |
1546 /** |
1000 * Filter the filesystem credentials form output. |
1547 * Filters the filesystem credentials form output. |
1001 * |
1548 * |
1002 * Returning anything other than an empty string will effectively short-circuit |
1549 * Returning anything other than an empty string will effectively short-circuit |
1003 * output of the filesystem credentials form, returning that value instead. |
1550 * output of the filesystem credentials form, returning that value instead. |
1004 * |
1551 * |
1005 * @since 2.5.0 |
1552 * @since 2.5.0 |
1006 * |
1553 * @since 4.6.0 The `$context` parameter default changed from `false` to an empty string. |
1007 * @param mixed $output Form output to return instead. Default empty. |
1554 * |
1008 * @param string $form_post URL to POST the form to. |
1555 * @param mixed $output Form output to return instead. Default empty. |
1009 * @param string $type Chosen type of filesystem. |
1556 * @param string $form_post The URL to post the form to. |
1010 * @param bool $error Whether the current request has failed to connect. |
1557 * @param string $type Chosen type of filesystem. |
1011 * Default false. |
1558 * @param bool $error Whether the current request has failed to connect. |
1012 * @param string $context Full path to the directory that is tested for |
1559 * Default false. |
1013 * being writable. |
1560 * @param string $context Full path to the directory that is tested for |
1014 * @param bool $allow_relaxed_file_ownership Whether to allow Group/World writable. |
1561 * being writable. |
1015 * @param array $extra_fields Extra POST fields. |
1562 * @param bool $allow_relaxed_file_ownership Whether to allow Group/World writable. |
|
1563 * Default false. |
|
1564 * @param array $extra_fields Extra POST fields. |
1016 */ |
1565 */ |
1017 $req_cred = apply_filters( 'request_filesystem_credentials', '', $form_post, $type, $error, $context, $extra_fields, $allow_relaxed_file_ownership ); |
1566 $req_cred = apply_filters( 'request_filesystem_credentials', '', $form_post, $type, $error, $context, $extra_fields, $allow_relaxed_file_ownership ); |
1018 if ( '' !== $req_cred ) |
1567 if ( '' !== $req_cred ) |
1019 return $req_cred; |
1568 return $req_cred; |
1020 |
1569 |
1161 <span class="field-title"><?php echo $label_pass; ?></span> |
1719 <span class="field-title"><?php echo $label_pass; ?></span> |
1162 <input name="password" type="password" id="password" value="<?php if ( defined('FTP_PASS') ) echo '*****'; ?>"<?php disabled( defined('FTP_PASS') ); ?> /> |
1720 <input name="password" type="password" id="password" value="<?php if ( defined('FTP_PASS') ) echo '*****'; ?>"<?php disabled( defined('FTP_PASS') ); ?> /> |
1163 <em><?php if ( ! defined('FTP_PASS') ) _e( 'This password will not be stored on the server.' ); ?></em> |
1721 <em><?php if ( ! defined('FTP_PASS') ) _e( 'This password will not be stored on the server.' ); ?></em> |
1164 </label> |
1722 </label> |
1165 </div> |
1723 </div> |
1166 <?php if ( isset($types['ssh']) ) : ?> |
1724 <fieldset> |
1167 <h4><?php _e('Authentication Keys') ?></h4> |
1725 <legend><?php _e( 'Connection Type' ); ?></legend> |
|
1726 <?php |
|
1727 $disabled = disabled( ( defined( 'FTP_SSL' ) && FTP_SSL ) || ( defined( 'FTP_SSH' ) && FTP_SSH ), true, false ); |
|
1728 foreach ( $types as $name => $text ) : ?> |
|
1729 <label for="<?php echo esc_attr( $name ) ?>"> |
|
1730 <input type="radio" name="connection_type" id="<?php echo esc_attr( $name ) ?>" value="<?php echo esc_attr( $name ) ?>"<?php checked( $name, $connection_type ); echo $disabled; ?> /> |
|
1731 <?php echo $text; ?> |
|
1732 </label> |
|
1733 <?php |
|
1734 endforeach; |
|
1735 ?> |
|
1736 </fieldset> |
|
1737 <?php |
|
1738 if ( isset( $types['ssh'] ) ) { |
|
1739 $hidden_class = ''; |
|
1740 if ( 'ssh' != $connection_type || empty( $connection_type ) ) { |
|
1741 $hidden_class = ' class="hidden"'; |
|
1742 } |
|
1743 ?> |
|
1744 <fieldset id="ssh-keys"<?php echo $hidden_class; ?>> |
|
1745 <legend><?php _e( 'Authentication Keys' ); ?></legend> |
1168 <label for="public_key"> |
1746 <label for="public_key"> |
1169 <span class="field-title"><?php _e('Public Key:') ?></span> |
1747 <span class="field-title"><?php _e('Public Key:') ?></span> |
1170 <input name="public_key" type="text" id="public_key" aria-describedby="auth-keys-desc" value="<?php echo esc_attr($public_key) ?>"<?php disabled( defined('FTP_PUBKEY') ); ?> /> |
1748 <input name="public_key" type="text" id="public_key" aria-describedby="auth-keys-desc" value="<?php echo esc_attr($public_key) ?>"<?php disabled( defined('FTP_PUBKEY') ); ?> /> |
1171 </label> |
1749 </label> |
1172 <label for="private_key"> |
1750 <label for="private_key"> |
1173 <span class="field-title"><?php _e('Private Key:') ?></span> |
1751 <span class="field-title"><?php _e('Private Key:') ?></span> |
1174 <input name="private_key" type="text" id="private_key" value="<?php echo esc_attr($private_key) ?>"<?php disabled( defined('FTP_PRIKEY') ); ?> /> |
1752 <input name="private_key" type="text" id="private_key" value="<?php echo esc_attr($private_key) ?>"<?php disabled( defined('FTP_PRIKEY') ); ?> /> |
1175 </label> |
1753 </label> |
1176 <span id="auth-keys-desc"><?php _e('Enter the location on the server where the public and private keys are located. If a passphrase is needed, enter that in the password field above.') ?></span> |
1754 <p id="auth-keys-desc"><?php _e( 'Enter the location on the server where the public and private keys are located. If a passphrase is needed, enter that in the password field above.' ) ?></p> |
1177 <?php endif; ?> |
|
1178 <h4><?php _e('Connection Type') ?></h4> |
|
1179 <fieldset><legend class="screen-reader-text"><span><?php _e('Connection Type') ?></span></legend> |
|
1180 <?php |
|
1181 $disabled = disabled( (defined('FTP_SSL') && FTP_SSL) || (defined('FTP_SSH') && FTP_SSH), true, false ); |
|
1182 foreach ( $types as $name => $text ) : ?> |
|
1183 <label for="<?php echo esc_attr($name) ?>"> |
|
1184 <input type="radio" name="connection_type" id="<?php echo esc_attr($name) ?>" value="<?php echo esc_attr($name) ?>"<?php checked($name, $connection_type); echo $disabled; ?> /> |
|
1185 <?php echo $text ?> |
|
1186 </label> |
|
1187 <?php endforeach; ?> |
|
1188 </fieldset> |
1755 </fieldset> |
1189 <?php |
1756 <?php |
|
1757 } |
|
1758 |
1190 foreach ( (array) $extra_fields as $field ) { |
1759 foreach ( (array) $extra_fields as $field ) { |
1191 if ( isset( $_POST[ $field ] ) ) |
1760 if ( isset( $submitted_form[ $field ] ) ) |
1192 echo '<input type="hidden" name="' . esc_attr( $field ) . '" value="' . esc_attr( wp_unslash( $_POST[ $field ] ) ) . '" />'; |
1761 echo '<input type="hidden" name="' . esc_attr( $field ) . '" value="' . esc_attr( $submitted_form[ $field ] ) . '" />'; |
1193 } |
1762 } |
1194 ?> |
1763 ?> |
1195 <p class="request-filesystem-credentials-action-buttons"> |
1764 <p class="request-filesystem-credentials-action-buttons"> |
|
1765 <?php wp_nonce_field( 'filesystem-credentials', '_fs_nonce', false, true ); ?> |
1196 <button class="button cancel-button" data-js-action="close" type="button"><?php _e( 'Cancel' ); ?></button> |
1766 <button class="button cancel-button" data-js-action="close" type="button"><?php _e( 'Cancel' ); ?></button> |
1197 <?php submit_button( __( 'Proceed' ), 'button', 'upgrade', false ); ?> |
1767 <?php submit_button( __( 'Proceed' ), '', 'upgrade', false ); ?> |
1198 </p> |
1768 </p> |
1199 </div> |
1769 </div> |
1200 </form> |
1770 </form> |
1201 <?php |
1771 <?php |
1202 return false; |
1772 return false; |
1220 <div id="request-filesystem-credentials-dialog" class="notification-dialog-wrap request-filesystem-credentials-dialog"> |
1790 <div id="request-filesystem-credentials-dialog" class="notification-dialog-wrap request-filesystem-credentials-dialog"> |
1221 <div class="notification-dialog-background"></div> |
1791 <div class="notification-dialog-background"></div> |
1222 <div class="notification-dialog" role="dialog" aria-labelledby="request-filesystem-credentials-title" tabindex="0"> |
1792 <div class="notification-dialog" role="dialog" aria-labelledby="request-filesystem-credentials-title" tabindex="0"> |
1223 <div class="request-filesystem-credentials-dialog-content"> |
1793 <div class="request-filesystem-credentials-dialog-content"> |
1224 <?php request_filesystem_credentials( site_url() ); ?> |
1794 <?php request_filesystem_credentials( site_url() ); ?> |
1225 <div> |
1795 </div> |
1226 </div> |
1796 </div> |
1227 </div> |
1797 </div> |
1228 <?php |
1798 <?php |
1229 } |
1799 } |
|
1800 |
|
1801 /** |
|
1802 * Generate a single group for the personal data export report. |
|
1803 * |
|
1804 * @since 4.9.6 |
|
1805 * |
|
1806 * @param array $group_data { |
|
1807 * The group data to render. |
|
1808 * |
|
1809 * @type string $group_label The user-facing heading for the group, e.g. 'Comments'. |
|
1810 * @type array $items { |
|
1811 * An array of group items. |
|
1812 * |
|
1813 * @type array $group_item_data { |
|
1814 * An array of name-value pairs for the item. |
|
1815 * |
|
1816 * @type string $name The user-facing name of an item name-value pair, e.g. 'IP Address'. |
|
1817 * @type string $value The user-facing value of an item data pair, e.g. '50.60.70.0'. |
|
1818 * } |
|
1819 * } |
|
1820 * } |
|
1821 * @return string The HTML for this group and its items. |
|
1822 */ |
|
1823 function wp_privacy_generate_personal_data_export_group_html( $group_data ) { |
|
1824 $allowed_tags = array( |
|
1825 'a' => array( |
|
1826 'href' => array(), |
|
1827 'target' => array() |
|
1828 ), |
|
1829 'br' => array() |
|
1830 ); |
|
1831 $allowed_protocols = array( 'http', 'https' ); |
|
1832 $group_html = ''; |
|
1833 |
|
1834 $group_html .= '<h2>' . esc_html( $group_data['group_label'] ) . '</h2>'; |
|
1835 $group_html .= '<div>'; |
|
1836 |
|
1837 foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) { |
|
1838 $group_html .= '<table>'; |
|
1839 $group_html .= '<tbody>'; |
|
1840 |
|
1841 foreach ( (array) $group_item_data as $group_item_datum ) { |
|
1842 $value = $group_item_datum['value']; |
|
1843 // If it looks like a link, make it a link |
|
1844 if ( false === strpos( $value, ' ' ) && ( 0 === strpos( $value, 'http://' ) || 0 === strpos( $value, 'https://' ) ) ) { |
|
1845 $value = '<a href="' . esc_url( $value ) . '">' . esc_html( $value ) . '</a>'; |
|
1846 } |
|
1847 |
|
1848 $group_html .= '<tr>'; |
|
1849 $group_html .= '<th>' . esc_html( $group_item_datum['name'] ) . '</th>'; |
|
1850 $group_html .= '<td>' . wp_kses( $value, $allowed_tags, $allowed_protocols ) . '</td>'; |
|
1851 $group_html .= '</tr>'; |
|
1852 } |
|
1853 |
|
1854 $group_html .= '</tbody>'; |
|
1855 $group_html .= '</table>'; |
|
1856 } |
|
1857 |
|
1858 $group_html .= '</div>'; |
|
1859 |
|
1860 return $group_html; |
|
1861 } |
|
1862 |
|
1863 /** |
|
1864 * Generate the personal data export file. |
|
1865 * |
|
1866 * @since 4.9.6 |
|
1867 * |
|
1868 * @param int $request_id The export request ID. |
|
1869 */ |
|
1870 function wp_privacy_generate_personal_data_export_file( $request_id ) { |
|
1871 if ( ! class_exists( 'ZipArchive' ) ) { |
|
1872 wp_send_json_error( __( 'Unable to generate export file. ZipArchive not available.' ) ); |
|
1873 } |
|
1874 |
|
1875 // Get the request data. |
|
1876 $request = wp_get_user_request_data( $request_id ); |
|
1877 |
|
1878 if ( ! $request || 'export_personal_data' !== $request->action_name ) { |
|
1879 wp_send_json_error( __( 'Invalid request ID when generating export file.' ) ); |
|
1880 } |
|
1881 |
|
1882 $email_address = $request->email; |
|
1883 |
|
1884 if ( ! is_email( $email_address ) ) { |
|
1885 wp_send_json_error( __( 'Invalid email address when generating export file.' ) ); |
|
1886 } |
|
1887 |
|
1888 // Create the exports folder if needed. |
|
1889 $exports_dir = wp_privacy_exports_dir(); |
|
1890 $exports_url = wp_privacy_exports_url(); |
|
1891 |
|
1892 if ( ! wp_mkdir_p( $exports_dir ) ) { |
|
1893 wp_send_json_error( __( 'Unable to create export folder.' ) ); |
|
1894 } |
|
1895 |
|
1896 // Protect export folder from browsing. |
|
1897 $index_pathname = $exports_dir . 'index.html'; |
|
1898 if ( ! file_exists( $index_pathname ) ) { |
|
1899 $file = fopen( $index_pathname, 'w' ); |
|
1900 if ( false === $file ) { |
|
1901 wp_send_json_error( __( 'Unable to protect export folder from browsing.' ) ); |
|
1902 } |
|
1903 fwrite( $file, '<!-- Silence is golden. -->' ); |
|
1904 fclose( $file ); |
|
1905 } |
|
1906 |
|
1907 $stripped_email = str_replace( '@', '-at-', $email_address ); |
|
1908 $stripped_email = sanitize_title( $stripped_email ); // slugify the email address |
|
1909 $obscura = wp_generate_password( 32, false, false ); |
|
1910 $file_basename = 'wp-personal-data-file-' . $stripped_email . '-' . $obscura; |
|
1911 $html_report_filename = $file_basename . '.html'; |
|
1912 $html_report_pathname = wp_normalize_path( $exports_dir . $html_report_filename ); |
|
1913 $file = fopen( $html_report_pathname, 'w' ); |
|
1914 if ( false === $file ) { |
|
1915 wp_send_json_error( __( 'Unable to open export file (HTML report) for writing.' ) ); |
|
1916 } |
|
1917 |
|
1918 $title = sprintf( |
|
1919 /* translators: %s: user's e-mail address */ |
|
1920 __( 'Personal Data Export for %s' ), |
|
1921 $email_address |
|
1922 ); |
|
1923 |
|
1924 // Open HTML. |
|
1925 fwrite( $file, "<!DOCTYPE html>\n" ); |
|
1926 fwrite( $file, "<html>\n" ); |
|
1927 |
|
1928 // Head. |
|
1929 fwrite( $file, "<head>\n" ); |
|
1930 fwrite( $file, "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />\n" ); |
|
1931 fwrite( $file, "<style type='text/css'>" ); |
|
1932 fwrite( $file, "body { color: black; font-family: Arial, sans-serif; font-size: 11pt; margin: 15px auto; width: 860px; }" ); |
|
1933 fwrite( $file, "table { background: #f0f0f0; border: 1px solid #ddd; margin-bottom: 20px; width: 100%; }" ); |
|
1934 fwrite( $file, "th { padding: 5px; text-align: left; width: 20%; }" ); |
|
1935 fwrite( $file, "td { padding: 5px; }" ); |
|
1936 fwrite( $file, "tr:nth-child(odd) { background-color: #fafafa; }" ); |
|
1937 fwrite( $file, "</style>" ); |
|
1938 fwrite( $file, "<title>" ); |
|
1939 fwrite( $file, esc_html( $title ) ); |
|
1940 fwrite( $file, "</title>" ); |
|
1941 fwrite( $file, "</head>\n" ); |
|
1942 |
|
1943 // Body. |
|
1944 fwrite( $file, "<body>\n" ); |
|
1945 |
|
1946 // Heading. |
|
1947 fwrite( $file, "<h1>" . esc_html__( 'Personal Data Export' ) . "</h1>" ); |
|
1948 |
|
1949 // And now, all the Groups. |
|
1950 $groups = get_post_meta( $request_id, '_export_data_grouped', true ); |
|
1951 |
|
1952 // First, build an "About" group on the fly for this report. |
|
1953 $about_group = array( |
|
1954 /* translators: Header for the About section in a personal data export. */ |
|
1955 'group_label' => _x( 'About', 'personal data group label' ), |
|
1956 'items' => array( |
|
1957 'about-1' => array( |
|
1958 array( |
|
1959 'name' => _x( 'Report generated for', 'email address' ), |
|
1960 'value' => $email_address, |
|
1961 ), |
|
1962 array( |
|
1963 'name' => _x( 'For site', 'website name' ), |
|
1964 'value' => get_bloginfo( 'name' ), |
|
1965 ), |
|
1966 array( |
|
1967 'name' => _x( 'At URL', 'website URL' ), |
|
1968 'value' => get_bloginfo( 'url' ), |
|
1969 ), |
|
1970 array( |
|
1971 'name' => _x( 'On', 'date/time' ), |
|
1972 'value' => current_time( 'mysql' ), |
|
1973 ), |
|
1974 ), |
|
1975 ), |
|
1976 ); |
|
1977 |
|
1978 // Merge in the special about group. |
|
1979 $groups = array_merge( array( 'about' => $about_group ), $groups ); |
|
1980 |
|
1981 // Now, iterate over every group in $groups and have the formatter render it in HTML. |
|
1982 foreach ( (array) $groups as $group_id => $group_data ) { |
|
1983 fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data ) ); |
|
1984 } |
|
1985 |
|
1986 fwrite( $file, "</body>\n" ); |
|
1987 |
|
1988 // Close HTML. |
|
1989 fwrite( $file, "</html>\n" ); |
|
1990 fclose( $file ); |
|
1991 |
|
1992 /* |
|
1993 * Now, generate the ZIP. |
|
1994 * |
|
1995 * If an archive has already been generated, then remove it and reuse the |
|
1996 * filename, to avoid breaking any URLs that may have been previously sent |
|
1997 * via email. |
|
1998 */ |
|
1999 $error = false; |
|
2000 $archive_url = get_post_meta( $request_id, '_export_file_url', true ); |
|
2001 $archive_pathname = get_post_meta( $request_id, '_export_file_path', true ); |
|
2002 |
|
2003 if ( empty( $archive_pathname ) || empty( $archive_url ) ) { |
|
2004 $archive_filename = $file_basename . '.zip'; |
|
2005 $archive_pathname = $exports_dir . $archive_filename; |
|
2006 $archive_url = $exports_url . $archive_filename; |
|
2007 |
|
2008 update_post_meta( $request_id, '_export_file_url', $archive_url ); |
|
2009 update_post_meta( $request_id, '_export_file_path', wp_normalize_path( $archive_pathname ) ); |
|
2010 } |
|
2011 |
|
2012 if ( ! empty( $archive_pathname ) && file_exists( $archive_pathname ) ) { |
|
2013 wp_delete_file( $archive_pathname ); |
|
2014 } |
|
2015 |
|
2016 $zip = new ZipArchive; |
|
2017 if ( true === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) { |
|
2018 if ( ! $zip->addFile( $html_report_pathname, 'index.html' ) ) { |
|
2019 $error = __( 'Unable to add data to export file.' ); |
|
2020 } |
|
2021 |
|
2022 $zip->close(); |
|
2023 |
|
2024 if ( ! $error ) { |
|
2025 /** |
|
2026 * Fires right after all personal data has been written to the export file. |
|
2027 * |
|
2028 * @since 4.9.6 |
|
2029 * |
|
2030 * @param string $archive_pathname The full path to the export file on the filesystem. |
|
2031 * @param string $archive_url The URL of the archive file. |
|
2032 * @param string $html_report_pathname The full path to the personal data report on the filesystem. |
|
2033 * @param int $request_id The export request ID. |
|
2034 */ |
|
2035 do_action( 'wp_privacy_personal_data_export_file_created', $archive_pathname, $archive_url, $html_report_pathname, $request_id ); |
|
2036 } |
|
2037 } else { |
|
2038 $error = __( 'Unable to open export file (archive) for writing.' ); |
|
2039 } |
|
2040 |
|
2041 // And remove the HTML file. |
|
2042 unlink( $html_report_pathname ); |
|
2043 |
|
2044 if ( $error ) { |
|
2045 wp_send_json_error( $error ); |
|
2046 } |
|
2047 } |
|
2048 |
|
2049 /** |
|
2050 * Send an email to the user with a link to the personal data export file |
|
2051 * |
|
2052 * @since 4.9.6 |
|
2053 * |
|
2054 * @param int $request_id The request ID for this personal data export. |
|
2055 * @return true|WP_Error True on success or `WP_Error` on failure. |
|
2056 */ |
|
2057 function wp_privacy_send_personal_data_export_email( $request_id ) { |
|
2058 // Get the request data. |
|
2059 $request = wp_get_user_request_data( $request_id ); |
|
2060 |
|
2061 if ( ! $request || 'export_personal_data' !== $request->action_name ) { |
|
2062 return new WP_Error( 'invalid', __( 'Invalid request ID when sending personal data export email.' ) ); |
|
2063 } |
|
2064 |
|
2065 /** This filter is documented in wp-includes/functions.php */ |
|
2066 $expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS ); |
|
2067 $expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration ); |
|
2068 |
|
2069 /* translators: Do not translate EXPIRATION, LINK, SITENAME, SITEURL: those are placeholders. */ |
|
2070 $email_text = __( |
|
2071 'Howdy, |
|
2072 |
|
2073 Your request for an export of personal data has been completed. You may |
|
2074 download your personal data by clicking on the link below. For privacy |
|
2075 and security, we will automatically delete the file on ###EXPIRATION###, |
|
2076 so please download it before then. |
|
2077 |
|
2078 ###LINK### |
|
2079 |
|
2080 Regards, |
|
2081 All at ###SITENAME### |
|
2082 ###SITEURL###' |
|
2083 ); |
|
2084 |
|
2085 /** |
|
2086 * Filters the text of the email sent with a personal data export file. |
|
2087 * |
|
2088 * The following strings have a special meaning and will get replaced dynamically: |
|
2089 * ###EXPIRATION### The date when the URL will be automatically deleted. |
|
2090 * ###LINK### URL of the personal data export file for the user. |
|
2091 * ###SITENAME### The name of the site. |
|
2092 * ###SITEURL### The URL to the site. |
|
2093 * |
|
2094 * @since 4.9.6 |
|
2095 * |
|
2096 * @param string $email_text Text in the email. |
|
2097 * @param int $request_id The request ID for this personal data export. |
|
2098 */ |
|
2099 $content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id ); |
|
2100 |
|
2101 $email_address = $request->email; |
|
2102 $export_file_url = get_post_meta( $request_id, '_export_file_url', true ); |
|
2103 $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); |
|
2104 $site_url = home_url(); |
|
2105 |
|
2106 $content = str_replace( '###EXPIRATION###', $expiration_date, $content ); |
|
2107 $content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content ); |
|
2108 $content = str_replace( '###EMAIL###', $email_address, $content ); |
|
2109 $content = str_replace( '###SITENAME###', $site_name, $content ); |
|
2110 $content = str_replace( '###SITEURL###', esc_url_raw( $site_url ), $content ); |
|
2111 |
|
2112 $mail_success = wp_mail( |
|
2113 $email_address, |
|
2114 sprintf( |
|
2115 __( '[%s] Personal Data Export' ), |
|
2116 $site_name |
|
2117 ), |
|
2118 $content |
|
2119 ); |
|
2120 |
|
2121 if ( ! $mail_success ) { |
|
2122 return new WP_Error( 'error', __( 'Unable to send personal data export email.' ) ); |
|
2123 } |
|
2124 |
|
2125 return true; |
|
2126 } |
|
2127 |
|
2128 /** |
|
2129 * Intercept personal data exporter page ajax responses in order to assemble the personal data export file. |
|
2130 * @see wp_privacy_personal_data_export_page |
|
2131 * @since 4.9.6 |
|
2132 * |
|
2133 * @param array $response The response from the personal data exporter for the given page. |
|
2134 * @param int $exporter_index The index of the personal data exporter. Begins at 1. |
|
2135 * @param string $email_address The email address of the user whose personal data this is. |
|
2136 * @param int $page The page of personal data for this exporter. Begins at 1. |
|
2137 * @param int $request_id The request ID for this personal data export. |
|
2138 * @param bool $send_as_email Whether the final results of the export should be emailed to the user. |
|
2139 * @param string $exporter_key The slug (key) of the exporter. |
|
2140 * @return array The filtered response. |
|
2141 */ |
|
2142 function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key ) { |
|
2143 /* Do some simple checks on the shape of the response from the exporter. |
|
2144 * If the exporter response is malformed, don't attempt to consume it - let it |
|
2145 * pass through to generate a warning to the user by default ajax processing. |
|
2146 */ |
|
2147 if ( ! is_array( $response ) ) { |
|
2148 return $response; |
|
2149 } |
|
2150 |
|
2151 if ( ! array_key_exists( 'done', $response ) ) { |
|
2152 return $response; |
|
2153 } |
|
2154 |
|
2155 if ( ! array_key_exists( 'data', $response ) ) { |
|
2156 return $response; |
|
2157 } |
|
2158 |
|
2159 if ( ! is_array( $response['data'] ) ) { |
|
2160 return $response; |
|
2161 } |
|
2162 |
|
2163 // Get the request data. |
|
2164 $request = wp_get_user_request_data( $request_id ); |
|
2165 |
|
2166 if ( ! $request || 'export_personal_data' !== $request->action_name ) { |
|
2167 wp_send_json_error( __( 'Invalid request ID when merging exporter data.' ) ); |
|
2168 } |
|
2169 |
|
2170 $export_data = array(); |
|
2171 |
|
2172 // First exporter, first page? Reset the report data accumulation array. |
|
2173 if ( 1 === $exporter_index && 1 === $page ) { |
|
2174 update_post_meta( $request_id, '_export_data_raw', $export_data ); |
|
2175 } else { |
|
2176 $export_data = get_post_meta( $request_id, '_export_data_raw', true ); |
|
2177 } |
|
2178 |
|
2179 // Now, merge the data from the exporter response into the data we have accumulated already. |
|
2180 $export_data = array_merge( $export_data, $response['data'] ); |
|
2181 update_post_meta( $request_id, '_export_data_raw', $export_data ); |
|
2182 |
|
2183 // If we are not yet on the last page of the last exporter, return now. |
|
2184 /** This filter is documented in wp-admin/includes/ajax-actions.php */ |
|
2185 $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() ); |
|
2186 $is_last_exporter = $exporter_index === count( $exporters ); |
|
2187 $exporter_done = $response['done']; |
|
2188 if ( ! $is_last_exporter || ! $exporter_done ) { |
|
2189 return $response; |
|
2190 } |
|
2191 |
|
2192 // Last exporter, last page - let's prepare the export file. |
|
2193 |
|
2194 // First we need to re-organize the raw data hierarchically in groups and items. |
|
2195 $groups = array(); |
|
2196 foreach ( (array) $export_data as $export_datum ) { |
|
2197 $group_id = $export_datum['group_id']; |
|
2198 $group_label = $export_datum['group_label']; |
|
2199 if ( ! array_key_exists( $group_id, $groups ) ) { |
|
2200 $groups[ $group_id ] = array( |
|
2201 'group_label' => $group_label, |
|
2202 'items' => array(), |
|
2203 ); |
|
2204 } |
|
2205 |
|
2206 $item_id = $export_datum['item_id']; |
|
2207 if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) { |
|
2208 $groups[ $group_id ]['items'][ $item_id ] = array(); |
|
2209 } |
|
2210 |
|
2211 $old_item_data = $groups[ $group_id ]['items'][ $item_id ]; |
|
2212 $merged_item_data = array_merge( $export_datum['data'], $old_item_data ); |
|
2213 $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data; |
|
2214 } |
|
2215 |
|
2216 // Then save the grouped data into the request. |
|
2217 delete_post_meta( $request_id, '_export_data_raw' ); |
|
2218 update_post_meta( $request_id, '_export_data_grouped', $groups ); |
|
2219 |
|
2220 /** |
|
2221 * Generate the export file from the collected, grouped personal data. |
|
2222 * |
|
2223 * @since 4.9.6 |
|
2224 * |
|
2225 * @param int $request_id The export request ID. |
|
2226 */ |
|
2227 do_action( 'wp_privacy_personal_data_export_file', $request_id ); |
|
2228 |
|
2229 // Clear the grouped data now that it is no longer needed. |
|
2230 delete_post_meta( $request_id, '_export_data_grouped' ); |
|
2231 |
|
2232 // If the destination is email, send it now. |
|
2233 if ( $send_as_email ) { |
|
2234 $mail_success = wp_privacy_send_personal_data_export_email( $request_id ); |
|
2235 if ( is_wp_error( $mail_success ) ) { |
|
2236 wp_send_json_error( $mail_success->get_error_message() ); |
|
2237 } |
|
2238 } else { |
|
2239 // Modify the response to include the URL of the export file so the browser can fetch it. |
|
2240 $export_file_url = get_post_meta( $request_id, '_export_file_url', true ); |
|
2241 if ( ! empty( $export_file_url ) ) { |
|
2242 $response['url'] = $export_file_url; |
|
2243 } |
|
2244 } |
|
2245 |
|
2246 // Update the request to completed state. |
|
2247 _wp_privacy_completed_request( $request_id ); |
|
2248 |
|
2249 return $response; |
|
2250 } |