wp/wp-admin/includes/file.php
changeset 7 cf61fcea0001
parent 5 5e2f62d02dcd
child 9 177826044cd9
equal deleted inserted replaced
6:490d5cc509ed 7:cf61fcea0001
     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  *
   144 function wp_tempnam( $filename = '', $dir = '' ) {
   622 function wp_tempnam( $filename = '', $dir = '' ) {
   145 	if ( empty( $dir ) ) {
   623 	if ( empty( $dir ) ) {
   146 		$dir = get_temp_dir();
   624 		$dir = get_temp_dir();
   147 	}
   625 	}
   148 
   626 
   149 	if ( empty( $filename ) || '.' == $filename || '/' == $filename ) {
   627 	if ( empty( $filename ) || '.' == $filename || '/' == $filename || '\\' == $filename ) {
   150 		$filename = time();
   628 		$filename = time();
   151 	}
   629 	}
   152 
   630 
   153 	// Use the basename of the given file without the extension as the name for the temporary directory
   631 	// Use the basename of the given file without the extension as the name for the temporary directory
   154 	$temp_filename = basename( $filename );
   632 	$temp_filename = basename( $filename );
   155 	$temp_filename = preg_replace( '|\.[^.]*$|', '', $temp_filename );
   633 	$temp_filename = preg_replace( '|\.[^.]*$|', '', $temp_filename );
   156 
   634 
   157 	// If the folder is falsey, use it's parent directory name instead
   635 	// If the folder is falsey, use its parent directory name instead.
   158 	if ( ! $temp_filename ) {
   636 	if ( ! $temp_filename ) {
   159 		return wp_tempnam( dirname( $filename ), $dir );
   637 		return wp_tempnam( dirname( $filename ), $dir );
   160 	}
   638 	}
   161 
   639 
       
   640 	// Suffix some random data to avoid filename conflicts
       
   641 	$temp_filename .= '-' . wp_generate_password( 6, false );
   162 	$temp_filename .= '.tmp';
   642 	$temp_filename .= '.tmp';
   163 	$temp_filename = $dir . wp_unique_filename( $dir, $temp_filename );
   643 	$temp_filename = $dir . wp_unique_filename( $dir, $temp_filename );
   164 	touch( $temp_filename );
   644 
       
   645 	$fp = @fopen( $temp_filename, 'x' );
       
   646 	if ( ! $fp && is_writable( $dir ) && file_exists( $temp_filename ) ) {
       
   647 		return wp_tempnam( $filename, $dir );
       
   648 	}
       
   649 	if ( $fp ) {
       
   650 		fclose( $fp );
       
   651 	}
   165 
   652 
   166 	return $temp_filename;
   653 	return $temp_filename;
   167 }
   654 }
   168 
   655 
   169 /**
   656 /**
   170  * Make sure that the file that was requested to edit, is allowed to be edited
   657  * Makes sure that the file that was requested to be edited is allowed to be edited.
   171  *
   658  *
   172  * Function will die if if you are not allowed to edit the file
   659  * Function will die if you are not allowed to edit the file.
   173  *
   660  *
   174  * @since 1.5.0
   661  * @since 1.5.0
   175  *
   662  *
   176  * @param string $file file the users is attempting to edit
   663  * @param string $file          File the user is attempting to edit.
   177  * @param array $allowed_files Array of allowed files to edit, $file must match an entry exactly
   664  * @param array  $allowed_files Optional. Array of allowed files to edit, $file must match an entry exactly.
   178  * @return string|null
   665  * @return string|null
   179  */
   666  */
   180 function validate_file_to_edit( $file, $allowed_files = '' ) {
   667 function validate_file_to_edit( $file, $allowed_files = array() ) {
   181 	$code = validate_file( $file, $allowed_files );
   668 	$code = validate_file( $file, $allowed_files );
   182 
   669 
   183 	if (!$code )
   670 	if (!$code )
   184 		return $file;
   671 		return $file;
   185 
   672 
   197 
   684 
   198 /**
   685 /**
   199  * Handle PHP uploads in WordPress, sanitizing file names, checking extensions for mime type,
   686  * Handle PHP uploads in WordPress, sanitizing file names, checking extensions for mime type,
   200  * and moving the file to the appropriate directory within the uploads directory.
   687  * and moving the file to the appropriate directory within the uploads directory.
   201  *
   688  *
       
   689  * @access private
   202  * @since 4.0.0
   690  * @since 4.0.0
   203  *
   691  *
   204  * @see wp_handle_upload_error
   692  * @see wp_handle_upload_error
   205  *
   693  *
   206  * @param array       $file      Reference to a single element of $_FILES. Call the function once for each uploaded file.
   694  * @param array       $file      Reference to a single element of $_FILES. Call the function once for each uploaded file.
   207  * @param array|false $overrides An associative array of names => values to override default variables. Default false.
   695  * @param array|false $overrides An associative array of names => values to override default variables. Default false.
   208  * @param string      $time      Time formatted in 'yyyy/mm'.
   696  * @param string      $time      Time formatted in 'yyyy/mm'.
   209  * @param string      $action    Expected value for $_POST['action'].
   697  * @param string      $action    Expected value for $_POST['action'].
   210  * @return array On success, returns an associative array of file attributes. On failure, returns
   698  * @return array On success, returns an associative array of file attributes. On failure, returns
   211  *               $overrides['upload_error_handler'](&$file, $message ) or array( 'error'=>$message ).
   699  *               $overrides['upload_error_handler'](&$file, $message ) or array( 'error'=>$message ).
   212 */
   700  */
   213 function _wp_handle_upload( &$file, $overrides, $time, $action ) {
   701 function _wp_handle_upload( &$file, $overrides, $time, $action ) {
   214 	// The default error handler.
   702 	// The default error handler.
   215 	if ( ! function_exists( 'wp_handle_upload_error' ) ) {
   703 	if ( ! function_exists( 'wp_handle_upload_error' ) ) {
   216 		function wp_handle_upload_error( &$file, $message ) {
   704 		function wp_handle_upload_error( &$file, $message ) {
   217 			return array( 'error' => $message );
   705 			return array( 'error' => $message );
   218 		}
   706 		}
   219 	}
   707 	}
   220 
   708 
   221 	/**
   709 	/**
   222 	 * Filter the data for a file before it is uploaded to WordPress.
   710 	 * Filters the data for a file before it is uploaded to WordPress.
   223 	 *
   711 	 *
   224 	 * The dynamic portion of the hook name, `$action`, refers to the post action.
   712 	 * The dynamic portion of the hook name, `$action`, refers to the post action.
   225 	 *
   713 	 *
   226 	 * @since 2.9.0 as 'wp_handle_upload_prefilter'.
   714 	 * @since 2.9.0 as 'wp_handle_upload_prefilter'.
   227 	 * @since 4.0.0 Converted to a dynamic hook with `$action`.
   715 	 * @since 4.0.0 Converted to a dynamic hook with `$action`.
   236 		$upload_error_handler = $overrides['upload_error_handler'];
   724 		$upload_error_handler = $overrides['upload_error_handler'];
   237 	}
   725 	}
   238 
   726 
   239 	// You may have had one or more 'wp_handle_upload_prefilter' functions error out the file. Handle that gracefully.
   727 	// You may have had one or more 'wp_handle_upload_prefilter' functions error out the file. Handle that gracefully.
   240 	if ( isset( $file['error'] ) && ! is_numeric( $file['error'] ) && $file['error'] ) {
   728 	if ( isset( $file['error'] ) && ! is_numeric( $file['error'] ) && $file['error'] ) {
   241 		return $upload_error_handler( $file, $file['error'] );
   729 		return call_user_func_array( $upload_error_handler, array( &$file, $file['error'] ) );
   242 	}
   730 	}
   243 
   731 
   244 	// Install user overrides. Did we mention that this voids your warranty?
   732 	// Install user overrides. Did we mention that this voids your warranty?
   245 
   733 
   246 	// You may define your own function and pass the name in $overrides['unique_filename_callback']
   734 	// You may define your own function and pass the name in $overrides['unique_filename_callback']
   278 	$test_type = isset( $overrides['test_type'] ) ? $overrides['test_type'] : true;
   766 	$test_type = isset( $overrides['test_type'] ) ? $overrides['test_type'] : true;
   279 	$mimes = isset( $overrides['mimes'] ) ? $overrides['mimes'] : false;
   767 	$mimes = isset( $overrides['mimes'] ) ? $overrides['mimes'] : false;
   280 
   768 
   281 	// A correct form post will pass this test.
   769 	// A correct form post will pass this test.
   282 	if ( $test_form && ( ! isset( $_POST['action'] ) || ( $_POST['action'] != $action ) ) ) {
   770 	if ( $test_form && ( ! isset( $_POST['action'] ) || ( $_POST['action'] != $action ) ) ) {
   283 		return call_user_func( $upload_error_handler, $file, __( 'Invalid form submission.' ) );
   771 		return call_user_func_array( $upload_error_handler, array( &$file, __( 'Invalid form submission.' ) ) );
   284 	}
   772 	}
   285 	// A successful upload will pass this test. It makes no sense to override this one.
   773 	// A successful upload will pass this test. It makes no sense to override this one.
   286 	if ( isset( $file['error'] ) && $file['error'] > 0 ) {
   774 	if ( isset( $file['error'] ) && $file['error'] > 0 ) {
   287 		return call_user_func( $upload_error_handler, $file, $upload_error_strings[ $file['error'] ] );
   775 		return call_user_func_array( $upload_error_handler, array( &$file, $upload_error_strings[ $file['error'] ] ) );
   288 	}
   776 	}
   289 
   777 
   290 	$test_file_size = 'wp_handle_upload' === $action ? $file['size'] : filesize( $file['tmp_name'] );
   778 	$test_file_size = 'wp_handle_upload' === $action ? $file['size'] : filesize( $file['tmp_name'] );
   291 	// A non-empty file will pass this test.
   779 	// A non-empty file will pass this test.
   292 	if ( $test_size && ! ( $test_file_size > 0 ) ) {
   780 	if ( $test_size && ! ( $test_file_size > 0 ) ) {
   293 		if ( is_multisite() ) {
   781 		if ( is_multisite() ) {
   294 			$error_msg = __( 'File is empty. Please upload something more substantial.' );
   782 			$error_msg = __( 'File is empty. Please upload something more substantial.' );
   295 		} else {
   783 		} else {
   296 			$error_msg = __( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.' );
   784 			$error_msg = __( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.' );
   297 		}
   785 		}
   298 		return call_user_func( $upload_error_handler, $file, $error_msg );
   786 		return call_user_func_array( $upload_error_handler, array( &$file, $error_msg ) );
   299 	}
   787 	}
   300 
   788 
   301 	// A properly uploaded file will pass this test. There should be no reason to override this one.
   789 	// A properly uploaded file will pass this test. There should be no reason to override this one.
   302 	$test_uploaded_file = 'wp_handle_upload' === $action ? @ is_uploaded_file( $file['tmp_name'] ) : @ is_file( $file['tmp_name'] );
   790 	$test_uploaded_file = 'wp_handle_upload' === $action ? @ is_uploaded_file( $file['tmp_name'] ) : @ is_file( $file['tmp_name'] );
   303 	if ( ! $test_uploaded_file ) {
   791 	if ( ! $test_uploaded_file ) {
   304 		return call_user_func( $upload_error_handler, $file, __( 'Specified file failed upload test.' ) );
   792 		return call_user_func_array( $upload_error_handler, array( &$file, __( 'Specified file failed upload test.' ) ) );
   305 	}
   793 	}
   306 
   794 
   307 	// A correct MIME type will pass this test. Override $mimes or use the upload_mimes filter.
   795 	// A correct MIME type will pass this test. Override $mimes or use the upload_mimes filter.
   308 	if ( $test_type ) {
   796 	if ( $test_type ) {
   309 		$wp_filetype = wp_check_filetype_and_ext( $file['tmp_name'], $file['name'], $mimes );
   797 		$wp_filetype = wp_check_filetype_and_ext( $file['tmp_name'], $file['name'], $mimes );
   314 		// Check to see if wp_check_filetype_and_ext() determined the filename was incorrect
   802 		// Check to see if wp_check_filetype_and_ext() determined the filename was incorrect
   315 		if ( $proper_filename ) {
   803 		if ( $proper_filename ) {
   316 			$file['name'] = $proper_filename;
   804 			$file['name'] = $proper_filename;
   317 		}
   805 		}
   318 		if ( ( ! $type || !$ext ) && ! current_user_can( 'unfiltered_upload' ) ) {
   806 		if ( ( ! $type || !$ext ) && ! current_user_can( 'unfiltered_upload' ) ) {
   319 			return call_user_func( $upload_error_handler, $file, __( 'Sorry, this file type is not permitted for security reasons.' ) );
   807 			return call_user_func_array( $upload_error_handler, array( &$file, __( 'Sorry, this file type is not permitted for security reasons.' ) ) );
   320 		}
   808 		}
   321 		if ( ! $type ) {
   809 		if ( ! $type ) {
   322 			$type = $file['type'];
   810 			$type = $file['type'];
   323 		}
   811 		}
   324 	} else {
   812 	} else {
   328 	/*
   816 	/*
   329 	 * A writable uploads dir will pass this test. Again, there's no point
   817 	 * A writable uploads dir will pass this test. Again, there's no point
   330 	 * overriding this one.
   818 	 * overriding this one.
   331 	 */
   819 	 */
   332 	if ( ! ( ( $uploads = wp_upload_dir( $time ) ) && false === $uploads['error'] ) ) {
   820 	if ( ! ( ( $uploads = wp_upload_dir( $time ) ) && false === $uploads['error'] ) ) {
   333 		return call_user_func( $upload_error_handler, $file, $uploads['error'] );
   821 		return call_user_func_array( $upload_error_handler, array( &$file, $uploads['error'] ) );
   334 	}
   822 	}
   335 
   823 
   336 	$filename = wp_unique_filename( $uploads['path'], $file['name'], $unique_filename_callback );
   824 	$filename = wp_unique_filename( $uploads['path'], $file['name'], $unique_filename_callback );
   337 
   825 
   338 	// Move the file to the uploads dir.
   826 	// Move the file to the uploads dir.
   339 	$new_file = $uploads['path'] . "/$filename";
   827 	$new_file = $uploads['path'] . "/$filename";
   340 	if ( 'wp_handle_upload' === $action ) {
   828 
   341 		$move_new_file = @ move_uploaded_file( $file['tmp_name'], $new_file );
   829  	/**
   342 	} else {
   830 	 * Filters whether to short-circuit moving the uploaded file after passing all checks.
   343 		$move_new_file = @ rename( $file['tmp_name'], $new_file );
   831 	 *
   344 	}
   832 	 * If a non-null value is passed to the filter, moving the file and any related error
   345 
   833 	 * reporting will be completely skipped.
   346 	if ( false === $move_new_file ) {
   834 	 *
   347 		if ( 0 === strpos( $uploads['basedir'], ABSPATH ) ) {
   835 	 * @since 4.9.0
   348 			$error_path = str_replace( ABSPATH, '', $uploads['basedir'] ) . $uploads['subdir'];
   836 	 *
       
   837 	 * @param string $move_new_file If null (default) move the file after the upload.
       
   838 	 * @param string $file          An array of data for a single file.
       
   839 	 * @param string $new_file      Filename of the newly-uploaded file.
       
   840 	 * @param string $type          File type.
       
   841 	 */
       
   842 	$move_new_file = apply_filters( 'pre_move_uploaded_file', null, $file, $new_file, $type );
       
   843 
       
   844 	if ( null === $move_new_file ) {
       
   845 		if ( 'wp_handle_upload' === $action ) {
       
   846 			$move_new_file = @ move_uploaded_file( $file['tmp_name'], $new_file );
   349 		} else {
   847 		} else {
   350 			$error_path = basename( $uploads['basedir'] ) . $uploads['subdir'];
   848 			// use copy and unlink because rename breaks streams.
   351 		}
   849 			$move_new_file = @ copy( $file['tmp_name'], $new_file );
   352 		return $upload_error_handler( $file, sprintf( __('The uploaded file could not be moved to %s.' ), $error_path ) );
   850 			unlink( $file['tmp_name'] );
       
   851 		}
       
   852 
       
   853 		if ( false === $move_new_file ) {
       
   854 			if ( 0 === strpos( $uploads['basedir'], ABSPATH ) ) {
       
   855 				$error_path = str_replace( ABSPATH, '', $uploads['basedir'] ) . $uploads['subdir'];
       
   856 			} else {
       
   857 				$error_path = basename( $uploads['basedir'] ) . $uploads['subdir'];
       
   858 			}
       
   859 			return $upload_error_handler( $file, sprintf( __('The uploaded file could not be moved to %s.' ), $error_path ) );
       
   860 		}
   353 	}
   861 	}
   354 
   862 
   355 	// Set correct file permissions.
   863 	// Set correct file permissions.
   356 	$stat = stat( dirname( $new_file ));
   864 	$stat = stat( dirname( $new_file ));
   357 	$perms = $stat['mode'] & 0000666;
   865 	$perms = $stat['mode'] & 0000666;
   363 	if ( is_multisite() ) {
   871 	if ( is_multisite() ) {
   364 		delete_transient( 'dirsize_cache' );
   872 		delete_transient( 'dirsize_cache' );
   365 	}
   873 	}
   366 
   874 
   367 	/**
   875 	/**
   368 	 * Filter the data array for the uploaded file.
   876 	 * Filters the data array for the uploaded file.
   369 	 *
   877 	 *
   370 	 * @since 2.1.0
   878 	 * @since 2.1.0
   371 	 *
   879 	 *
   372 	 * @param array  $upload {
   880 	 * @param array  $upload {
   373 	 *     Array of upload data.
   881 	 *     Array of upload data.
   380 	 */
   888 	 */
   381 	return apply_filters( 'wp_handle_upload', array(
   889 	return apply_filters( 'wp_handle_upload', array(
   382 		'file' => $new_file,
   890 		'file' => $new_file,
   383 		'url'  => $url,
   891 		'url'  => $url,
   384 		'type' => $type
   892 		'type' => $type
   385 	), 'wp_handle_sideload' === $action ? 'sideload' : 'upload' ); }
   893 	), 'wp_handle_sideload' === $action ? 'sideload' : 'upload' );
   386 
   894 }
   387 /**
   895 
   388  * Wrapper for _wp_handle_upload(), passes 'wp_handle_upload' action.
   896 /**
       
   897  * Wrapper for _wp_handle_upload().
       
   898  *
       
   899  * Passes the {@see 'wp_handle_upload'} action.
   389  *
   900  *
   390  * @since 2.0.0
   901  * @since 2.0.0
   391  *
   902  *
   392  * @see _wp_handle_upload()
   903  * @see _wp_handle_upload()
   393  *
   904  *
   394  * @param array      $file      Reference to a single element of $_FILES. Call the function once for
   905  * @param array      $file      Reference to a single element of `$_FILES`. Call the function once for
   395  *                              each uploaded file.
   906  *                              each uploaded file.
   396  * @param array|bool $overrides Optional. An associative array of names=>values to override default
   907  * @param array|bool $overrides Optional. An associative array of names=>values to override default
   397  *                              variables. Default false.
   908  *                              variables. Default false.
   398  * @param string     $time      Optional. Time formatted in 'yyyy/mm'. Default null.
   909  * @param string     $time      Optional. Time formatted in 'yyyy/mm'. Default null.
   399  * @return array On success, returns an associative array of file attributes. On failure, returns
   910  * @return array On success, returns an associative array of file attributes. On failure, returns
   411 
   922 
   412 	return _wp_handle_upload( $file, $overrides, $time, $action );
   923 	return _wp_handle_upload( $file, $overrides, $time, $action );
   413 }
   924 }
   414 
   925 
   415 /**
   926 /**
   416  * Wrapper for _wp_handle_upload(), passes 'wp_handle_sideload' action
   927  * Wrapper for _wp_handle_upload().
       
   928  *
       
   929  * Passes the {@see 'wp_handle_sideload'} action.
   417  *
   930  *
   418  * @since 2.6.0
   931  * @since 2.6.0
   419  *
   932  *
   420  * @see _wp_handle_upload()
   933  * @see _wp_handle_upload()
   421  *
   934  *
   422  * @param array      $file      An array similar to that of a PHP $_FILES POST array
   935  * @param array      $file      An array similar to that of a PHP `$_FILES` POST array
   423  * @param array|bool $overrides Optional. An associative array of names=>values to override default
   936  * @param array|bool $overrides Optional. An associative array of names=>values to override default
   424  *                              variables. Default false.
   937  *                              variables. Default false.
   425  * @param string     $time      Optional. Time formatted in 'yyyy/mm'. Default null.
   938  * @param string     $time      Optional. Time formatted in 'yyyy/mm'. Default null.
   426  * @return array On success, returns an associative array of file attributes. On failure, returns
   939  * @return array On success, returns an associative array of file attributes. On failure, returns
   427  *               $overrides['upload_error_handler'](&$file, $message ) or array( 'error'=>$message ).
   940  *               $overrides['upload_error_handler'](&$file, $message ) or array( 'error'=>$message ).
   438 	return _wp_handle_upload( $file, $overrides, $time, $action );
   951 	return _wp_handle_upload( $file, $overrides, $time, $action );
   439 }
   952 }
   440 
   953 
   441 
   954 
   442 /**
   955 /**
   443  * Downloads a url to a local temporary file using the WordPress HTTP Class.
   956  * Downloads a URL to a local temporary file using the WordPress HTTP Class.
   444  * Please note, That the calling function must unlink() the file.
   957  * Please note, That the calling function must unlink() the file.
   445  *
   958  *
   446  * @since 2.5.0
   959  * @since 2.5.0
   447  *
   960  *
   448  * @param string $url the URL of the file to download
   961  * @param string $url the URL of the file to download
   452 function download_url( $url, $timeout = 300 ) {
   965 function download_url( $url, $timeout = 300 ) {
   453 	//WARNING: The file is not automatically deleted, The script must unlink() the file.
   966 	//WARNING: The file is not automatically deleted, The script must unlink() the file.
   454 	if ( ! $url )
   967 	if ( ! $url )
   455 		return new WP_Error('http_no_url', __('Invalid URL Provided.'));
   968 		return new WP_Error('http_no_url', __('Invalid URL Provided.'));
   456 
   969 
   457 	$tmpfname = wp_tempnam($url);
   970 	$url_filename = basename( parse_url( $url, PHP_URL_PATH ) );
       
   971 
       
   972 	$tmpfname = wp_tempnam( $url_filename );
   458 	if ( ! $tmpfname )
   973 	if ( ! $tmpfname )
   459 		return new WP_Error('http_no_file', __('Could not create Temporary file.'));
   974 		return new WP_Error('http_no_file', __('Could not create Temporary file.'));
   460 
   975 
   461 	$response = wp_safe_remote_get( $url, array( 'timeout' => $timeout, 'stream' => true, 'filename' => $tmpfname ) );
   976 	$response = wp_safe_remote_get( $url, array( 'timeout' => $timeout, 'stream' => true, 'filename' => $tmpfname ) );
   462 
   977 
   514  * Attempts to increase the PHP Memory limit to 256M before uncompressing,
  1029  * Attempts to increase the PHP Memory limit to 256M before uncompressing,
   515  * However, The most memory required shouldn't be much larger than the Archive itself.
  1030  * However, The most memory required shouldn't be much larger than the Archive itself.
   516  *
  1031  *
   517  * @since 2.5.0
  1032  * @since 2.5.0
   518  *
  1033  *
       
  1034  * @global WP_Filesystem_Base $wp_filesystem Subclass
       
  1035  *
   519  * @param string $file Full path and filename of zip archive
  1036  * @param string $file Full path and filename of zip archive
   520  * @param string $to Full path on the filesystem to extract archive to
  1037  * @param string $to Full path on the filesystem to extract archive to
   521  * @return mixed WP_Error on failure, True on success
  1038  * @return mixed WP_Error on failure, True on success
   522  */
  1039  */
   523 function unzip_file($file, $to) {
  1040 function unzip_file($file, $to) {
   524 	global $wp_filesystem;
  1041 	global $wp_filesystem;
   525 
  1042 
   526 	if ( ! $wp_filesystem || !is_object($wp_filesystem) )
  1043 	if ( ! $wp_filesystem || !is_object($wp_filesystem) )
   527 		return new WP_Error('fs_unavailable', __('Could not access filesystem.'));
  1044 		return new WP_Error('fs_unavailable', __('Could not access filesystem.'));
   528 
  1045 
   529 	// Unzip can use a lot of memory, but not this much hopefully
  1046 	// Unzip can use a lot of memory, but not this much hopefully.
   530 	/** This filter is documented in wp-admin/admin.php */
  1047 	wp_raise_memory_limit( 'admin' );
   531 	@ini_set( 'memory_limit', apply_filters( 'admin_memory_limit', WP_MAX_MEMORY_LIMIT ) );
       
   532 
  1048 
   533 	$needed_dirs = array();
  1049 	$needed_dirs = array();
   534 	$to = trailingslashit($to);
  1050 	$to = trailingslashit($to);
   535 
  1051 
   536 	// Determine any parent dir's needed (of the upgrade directory)
  1052 	// Determine any parent dir's needed (of the upgrade directory)
   550 				break; // A folder exists, therefor, we dont need the check the levels below this
  1066 				break; // A folder exists, therefor, we dont need the check the levels below this
   551 		}
  1067 		}
   552 	}
  1068 	}
   553 
  1069 
   554 	/**
  1070 	/**
   555 	 * Filter whether to use ZipArchive to unzip archives.
  1071 	 * Filters whether to use ZipArchive to unzip archives.
   556 	 *
  1072 	 *
   557 	 * @since 3.0.0
  1073 	 * @since 3.0.0
   558 	 *
  1074 	 *
   559 	 * @param bool $ziparchive Whether to use ZipArchive. Default true.
  1075 	 * @param bool $ziparchive Whether to use ZipArchive. Default true.
   560 	 */
  1076 	 */
   561 	if ( class_exists( 'ZipArchive' ) && apply_filters( 'unzip_file_use_ziparchive', true ) ) {
  1077 	if ( class_exists( 'ZipArchive', false ) && apply_filters( 'unzip_file_use_ziparchive', true ) ) {
   562 		$result = _unzip_file_ziparchive($file, $to, $needed_dirs);
  1078 		$result = _unzip_file_ziparchive($file, $to, $needed_dirs);
   563 		if ( true === $result ) {
  1079 		if ( true === $result ) {
   564 			return $result;
  1080 			return $result;
   565 		} elseif ( is_wp_error($result) ) {
  1081 		} elseif ( is_wp_error($result) ) {
   566 			if ( 'incompatible_archive' != $result->get_error_code() )
  1082 			if ( 'incompatible_archive' != $result->get_error_code() )
   577  *
  1093  *
   578  * @since 3.0.0
  1094  * @since 3.0.0
   579  * @see unzip_file
  1095  * @see unzip_file
   580  * @access private
  1096  * @access private
   581  *
  1097  *
       
  1098  * @global WP_Filesystem_Base $wp_filesystem Subclass
       
  1099  *
   582  * @param string $file Full path and filename of zip archive
  1100  * @param string $file Full path and filename of zip archive
   583  * @param string $to Full path on the filesystem to extract archive to
  1101  * @param string $to Full path on the filesystem to extract archive to
   584  * @param array $needed_dirs A partial list of required folders needed to be created.
  1102  * @param array $needed_dirs A partial list of required folders needed to be created.
   585  * @return mixed WP_Error on failure, True on success
  1103  * @return mixed WP_Error on failure, True on success
   586  */
  1104  */
   600 			return new WP_Error( 'stat_failed_ziparchive', __( 'Could not retrieve file from archive.' ) );
  1118 			return new WP_Error( 'stat_failed_ziparchive', __( 'Could not retrieve file from archive.' ) );
   601 
  1119 
   602 		if ( '__MACOSX/' === substr($info['name'], 0, 9) ) // Skip the OS X-created __MACOSX directory
  1120 		if ( '__MACOSX/' === substr($info['name'], 0, 9) ) // Skip the OS X-created __MACOSX directory
   603 			continue;
  1121 			continue;
   604 
  1122 
       
  1123 		// Don't extract invalid files:
       
  1124 		if ( 0 !== validate_file( $info['name'] ) ) {
       
  1125 			continue;
       
  1126 		}
       
  1127 
   605 		$uncompressed_size += $info['size'];
  1128 		$uncompressed_size += $info['size'];
   606 
  1129 
   607 		if ( '/' == substr($info['name'], -1) ) // directory
  1130 		if ( '/' === substr( $info['name'], -1 ) ) {
   608 			$needed_dirs[] = $to . untrailingslashit($info['name']);
  1131 			// Directory.
   609 		else
  1132 			$needed_dirs[] = $to . untrailingslashit( $info['name'] );
   610 			$needed_dirs[] = $to . untrailingslashit(dirname($info['name']));
  1133 		} elseif ( '.' !== $dirname = dirname( $info['name'] ) ) {
       
  1134 			// Path to a file.
       
  1135 			$needed_dirs[] = $to . untrailingslashit( $dirname );
       
  1136 		}
   611 	}
  1137 	}
   612 
  1138 
   613 	/*
  1139 	/*
   614 	 * disk_free_space() could return false. Assume that any falsey value is an error.
  1140 	 * disk_free_space() could return false. Assume that any falsey value is an error.
   615 	 * A disk that has zero free bytes has bigger problems.
  1141 	 * A disk that has zero free bytes has bigger problems.
   616 	 * Require we have enough space to unzip the file and copy its contents, with a 10% buffer.
  1142 	 * Require we have enough space to unzip the file and copy its contents, with a 10% buffer.
   617 	 */
  1143 	 */
   618 	if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
  1144 	if ( wp_doing_cron() ) {
   619 		$available_space = @disk_free_space( WP_CONTENT_DIR );
  1145 		$available_space = @disk_free_space( WP_CONTENT_DIR );
   620 		if ( $available_space && ( $uncompressed_size * 2.1 ) > $available_space )
  1146 		if ( $available_space && ( $uncompressed_size * 2.1 ) > $available_space )
   621 			return new WP_Error( 'disk_full_unzip_file', __( 'Could not copy files. You may have run out of disk space.' ), compact( 'uncompressed_size', 'available_space' ) );
  1147 			return new WP_Error( 'disk_full_unzip_file', __( 'Could not copy files. You may have run out of disk space.' ), compact( 'uncompressed_size', 'available_space' ) );
   622 	}
  1148 	}
   623 
  1149 
   654 			continue;
  1180 			continue;
   655 
  1181 
   656 		if ( '__MACOSX/' === substr($info['name'], 0, 9) ) // Don't extract the OS X-created __MACOSX directory files
  1182 		if ( '__MACOSX/' === substr($info['name'], 0, 9) ) // Don't extract the OS X-created __MACOSX directory files
   657 			continue;
  1183 			continue;
   658 
  1184 
       
  1185 		// Don't extract invalid files:
       
  1186 		if ( 0 !== validate_file( $info['name'] ) ) {
       
  1187 			continue;
       
  1188 		}
       
  1189 
   659 		$contents = $z->getFromIndex($i);
  1190 		$contents = $z->getFromIndex($i);
   660 		if ( false === $contents )
  1191 		if ( false === $contents )
   661 			return new WP_Error( 'extract_failed_ziparchive', __( 'Could not extract file from archive.' ), $info['name'] );
  1192 			return new WP_Error( 'extract_failed_ziparchive', __( 'Could not extract file from archive.' ), $info['name'] );
   662 
  1193 
   663 		if ( ! $wp_filesystem->put_contents( $to . $info['name'], $contents, FS_CHMOD_FILE) )
  1194 		if ( ! $wp_filesystem->put_contents( $to . $info['name'], $contents, FS_CHMOD_FILE) )
   674  * Assumes that WP_Filesystem() has already been called and set up.
  1205  * Assumes that WP_Filesystem() has already been called and set up.
   675  *
  1206  *
   676  * @since 3.0.0
  1207  * @since 3.0.0
   677  * @see unzip_file
  1208  * @see unzip_file
   678  * @access private
  1209  * @access private
       
  1210  *
       
  1211  * @global WP_Filesystem_Base $wp_filesystem Subclass
   679  *
  1212  *
   680  * @param string $file Full path and filename of zip archive
  1213  * @param string $file Full path and filename of zip archive
   681  * @param string $to Full path on the filesystem to extract archive to
  1214  * @param string $to Full path on the filesystem to extract archive to
   682  * @param array $needed_dirs A partial list of required folders needed to be created.
  1215  * @param array $needed_dirs A partial list of required folders needed to be created.
   683  * @return mixed WP_Error on failure, True on success
  1216  * @return mixed WP_Error on failure, True on success
   717 	/*
  1250 	/*
   718 	 * disk_free_space() could return false. Assume that any falsey value is an error.
  1251 	 * disk_free_space() could return false. Assume that any falsey value is an error.
   719 	 * A disk that has zero free bytes has bigger problems.
  1252 	 * A disk that has zero free bytes has bigger problems.
   720 	 * Require we have enough space to unzip the file and copy its contents, with a 10% buffer.
  1253 	 * Require we have enough space to unzip the file and copy its contents, with a 10% buffer.
   721 	 */
  1254 	 */
   722 	if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
  1255 	if ( wp_doing_cron() ) {
   723 		$available_space = @disk_free_space( WP_CONTENT_DIR );
  1256 		$available_space = @disk_free_space( WP_CONTENT_DIR );
   724 		if ( $available_space && ( $uncompressed_size * 2.1 ) > $available_space )
  1257 		if ( $available_space && ( $uncompressed_size * 2.1 ) > $available_space )
   725 			return new WP_Error( 'disk_full_unzip_file', __( 'Could not copy files. You may have run out of disk space.' ), compact( 'uncompressed_size', 'available_space' ) );
  1258 			return new WP_Error( 'disk_full_unzip_file', __( 'Could not copy files. You may have run out of disk space.' ), compact( 'uncompressed_size', 'available_space' ) );
   726 	}
  1259 	}
   727 
  1260 
   755 			continue;
  1288 			continue;
   756 
  1289 
   757 		if ( '__MACOSX/' === substr($file['filename'], 0, 9) ) // Don't extract the OS X-created __MACOSX directory files
  1290 		if ( '__MACOSX/' === substr($file['filename'], 0, 9) ) // Don't extract the OS X-created __MACOSX directory files
   758 			continue;
  1291 			continue;
   759 
  1292 
       
  1293 		// Don't extract invalid files:
       
  1294 		if ( 0 !== validate_file( $file['filename'] ) ) {
       
  1295 			continue;
       
  1296 		}
       
  1297 
   760 		if ( ! $wp_filesystem->put_contents( $to . $file['filename'], $file['content'], FS_CHMOD_FILE) )
  1298 		if ( ! $wp_filesystem->put_contents( $to . $file['filename'], $file['content'], FS_CHMOD_FILE) )
   761 			return new WP_Error( 'copy_failed_pclzip', __( 'Could not copy file.' ), $file['filename'] );
  1299 			return new WP_Error( 'copy_failed_pclzip', __( 'Could not copy file.' ), $file['filename'] );
   762 	}
  1300 	}
   763 	return true;
  1301 	return true;
   764 }
  1302 }
   766 /**
  1304 /**
   767  * Copies a directory from one location to another via the WordPress Filesystem Abstraction.
  1305  * Copies a directory from one location to another via the WordPress Filesystem Abstraction.
   768  * Assumes that WP_Filesystem() has already been called and setup.
  1306  * Assumes that WP_Filesystem() has already been called and setup.
   769  *
  1307  *
   770  * @since 2.5.0
  1308  * @since 2.5.0
       
  1309  *
       
  1310  * @global WP_Filesystem_Base $wp_filesystem Subclass
   771  *
  1311  *
   772  * @param string $from source directory
  1312  * @param string $from source directory
   773  * @param string $to destination directory
  1313  * @param string $to destination directory
   774  * @param array $skip_list a list of files/folders to skip copying
  1314  * @param array $skip_list a list of files/folders to skip copying
   775  * @return mixed WP_Error on failure, True on success.
  1315  * @return mixed WP_Error on failure, True on success.
   821  * Plugins may add extra transports, And force WordPress to use them by returning
  1361  * Plugins may add extra transports, And force WordPress to use them by returning
   822  * the filename via the {@see 'filesystem_method_file'} filter.
  1362  * the filename via the {@see 'filesystem_method_file'} filter.
   823  *
  1363  *
   824  * @since 2.5.0
  1364  * @since 2.5.0
   825  *
  1365  *
   826  * @param array  $args                         Optional. Connection args, These are passed directly to
  1366  * @global WP_Filesystem_Base $wp_filesystem Subclass
   827  *                                             the `WP_Filesystem_*()` classes. Default false.
  1367  *
   828  * @param string $context                      Optional. Context for {@see get_filesystem_method()}.
  1368  * @param array|false  $args                         Optional. Connection args, These are passed directly to
   829  *                                             Default false.
  1369  *                                                   the `WP_Filesystem_*()` classes. Default false.
   830  * @param bool   $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable.
  1370  * @param string|false $context                      Optional. Context for get_filesystem_method(). Default false.
   831  *                                             Default false.
  1371  * @param bool         $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable. Default false.
   832  * @return null|boolean false on failure, true on success.
  1372  * @return null|bool false on failure, true on success.
   833  */
  1373  */
   834 function WP_Filesystem( $args = false, $context = false, $allow_relaxed_file_ownership = false ) {
  1374 function WP_Filesystem( $args = false, $context = false, $allow_relaxed_file_ownership = false ) {
   835 	global $wp_filesystem;
  1375 	global $wp_filesystem;
   836 
  1376 
   837 	require_once(ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php');
  1377 	require_once(ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php');
   839 	$method = get_filesystem_method( $args, $context, $allow_relaxed_file_ownership );
  1379 	$method = get_filesystem_method( $args, $context, $allow_relaxed_file_ownership );
   840 
  1380 
   841 	if ( ! $method )
  1381 	if ( ! $method )
   842 		return false;
  1382 		return false;
   843 
  1383 
   844 	if ( ! class_exists("WP_Filesystem_$method") ) {
  1384 	if ( ! class_exists( "WP_Filesystem_$method" ) ) {
   845 
  1385 
   846 		/**
  1386 		/**
   847 		 * Filter the path for a specific filesystem method class file.
  1387 		 * Filters the path for a specific filesystem method class file.
   848 		 *
  1388 		 *
   849 		 * @since 2.6.0
  1389 		 * @since 2.6.0
   850 		 *
  1390 		 *
   851 		 * @see get_filesystem_method()
  1391 		 * @see get_filesystem_method()
   852 		 *
  1392 		 *
   900  *
  1440  *
   901  * Plugins may define a custom transport handler, See WP_Filesystem().
  1441  * Plugins may define a custom transport handler, See WP_Filesystem().
   902  *
  1442  *
   903  * @since 2.5.0
  1443  * @since 2.5.0
   904  *
  1444  *
       
  1445  * @global callable $_wp_filesystem_direct_method
       
  1446  *
   905  * @param array  $args                         Optional. Connection details. Default empty array.
  1447  * @param array  $args                         Optional. Connection details. Default empty array.
   906  * @param string $context                      Optional. Full path to the directory that is tested
  1448  * @param string $context                      Optional. Full path to the directory that is tested
   907  *                                             for being writable. Default false.
  1449  *                                             for being writable. Default empty.
   908  * @param bool   $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable.
  1450  * @param bool   $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable.
   909  *                                             Default false.
  1451  *                                             Default false.
   910  * @return string The transport to use, see description for valid return values.
  1452  * @return string The transport to use, see description for valid return values.
   911  */
  1453  */
   912 function get_filesystem_method( $args = array(), $context = false, $allow_relaxed_file_ownership = false ) {
  1454 function get_filesystem_method( $args = array(), $context = '', $allow_relaxed_file_ownership = false ) {
   913 	$method = defined('FS_METHOD') ? FS_METHOD : false; // Please ensure that this is either 'direct', 'ssh2', 'ftpext' or 'ftpsockets'
  1455 	$method = defined('FS_METHOD') ? FS_METHOD : false; // Please ensure that this is either 'direct', 'ssh2', 'ftpext' or 'ftpsockets'
   914 
  1456 
   915 	if ( ! $context ) {
  1457 	if ( ! $context ) {
   916 		$context = WP_CONTENT_DIR;
  1458 		$context = WP_CONTENT_DIR;
   917 	}
  1459 	}
   956 	if ( ! $method && isset($args['connection_type']) && 'ssh' == $args['connection_type'] && extension_loaded('ssh2') && function_exists('stream_get_contents') ) $method = 'ssh2';
  1498 	if ( ! $method && isset($args['connection_type']) && 'ssh' == $args['connection_type'] && extension_loaded('ssh2') && function_exists('stream_get_contents') ) $method = 'ssh2';
   957 	if ( ! $method && extension_loaded('ftp') ) $method = 'ftpext';
  1499 	if ( ! $method && extension_loaded('ftp') ) $method = 'ftpext';
   958 	if ( ! $method && ( extension_loaded('sockets') || function_exists('fsockopen') ) ) $method = 'ftpsockets'; //Sockets: Socket extension; PHP Mode: FSockopen / fwrite / fread
  1500 	if ( ! $method && ( extension_loaded('sockets') || function_exists('fsockopen') ) ) $method = 'ftpsockets'; //Sockets: Socket extension; PHP Mode: FSockopen / fwrite / fread
   959 
  1501 
   960 	/**
  1502 	/**
   961 	 * Filter the filesystem method to use.
  1503 	 * Filters the filesystem method to use.
   962 	 *
  1504 	 *
   963 	 * @since 2.6.0
  1505 	 * @since 2.6.0
   964 	 *
  1506 	 *
   965 	 * @param string $method  Filesystem method to return.
  1507 	 * @param string $method  Filesystem method to return.
   966 	 * @param array  $args    An array of connection details for the method.
  1508 	 * @param array  $args    An array of connection details for the method.
   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 
  1028 	if ( is_null( $extra_fields ) )
  1577 	if ( is_null( $extra_fields ) )
  1029 		$extra_fields = array( 'version', 'locale' );
  1578 		$extra_fields = array( 'version', 'locale' );
  1030 
  1579 
  1031 	$credentials = get_option('ftp_credentials', array( 'hostname' => '', 'username' => ''));
  1580 	$credentials = get_option('ftp_credentials', array( 'hostname' => '', 'username' => ''));
  1032 
  1581 
       
  1582 	$submitted_form = wp_unslash( $_POST );
       
  1583 
       
  1584 	// Verify nonce, or unset submitted form field values on failure
       
  1585 	if ( ! isset( $_POST['_fs_nonce'] ) || ! wp_verify_nonce( $_POST['_fs_nonce'], 'filesystem-credentials' ) ) {
       
  1586 		unset(
       
  1587 			$submitted_form['hostname'],
       
  1588 			$submitted_form['username'],
       
  1589 			$submitted_form['password'],
       
  1590 			$submitted_form['public_key'],
       
  1591 			$submitted_form['private_key'],
       
  1592 			$submitted_form['connection_type']
       
  1593 		);
       
  1594 	}
       
  1595 
  1033 	// If defined, set it to that, Else, If POST'd, set it to that, If not, Set it to whatever it previously was(saved details in option)
  1596 	// If defined, set it to that, Else, If POST'd, set it to that, If not, Set it to whatever it previously was(saved details in option)
  1034 	$credentials['hostname'] = defined('FTP_HOST') ? FTP_HOST : (!empty($_POST['hostname']) ? wp_unslash( $_POST['hostname'] ) : $credentials['hostname']);
  1597 	$credentials['hostname'] = defined('FTP_HOST') ? FTP_HOST : (!empty($submitted_form['hostname']) ? $submitted_form['hostname'] : $credentials['hostname']);
  1035 	$credentials['username'] = defined('FTP_USER') ? FTP_USER : (!empty($_POST['username']) ? wp_unslash( $_POST['username'] ) : $credentials['username']);
  1598 	$credentials['username'] = defined('FTP_USER') ? FTP_USER : (!empty($submitted_form['username']) ? $submitted_form['username'] : $credentials['username']);
  1036 	$credentials['password'] = defined('FTP_PASS') ? FTP_PASS : (!empty($_POST['password']) ? wp_unslash( $_POST['password'] ) : '');
  1599 	$credentials['password'] = defined('FTP_PASS') ? FTP_PASS : (!empty($submitted_form['password']) ? $submitted_form['password'] : '');
  1037 
  1600 
  1038 	// Check to see if we are setting the public/private keys for ssh
  1601 	// Check to see if we are setting the public/private keys for ssh
  1039 	$credentials['public_key'] = defined('FTP_PUBKEY') ? FTP_PUBKEY : (!empty($_POST['public_key']) ? wp_unslash( $_POST['public_key'] ) : '');
  1602 	$credentials['public_key'] = defined('FTP_PUBKEY') ? FTP_PUBKEY : (!empty($submitted_form['public_key']) ? $submitted_form['public_key'] : '');
  1040 	$credentials['private_key'] = defined('FTP_PRIKEY') ? FTP_PRIKEY : (!empty($_POST['private_key']) ? wp_unslash( $_POST['private_key'] ) : '');
  1603 	$credentials['private_key'] = defined('FTP_PRIKEY') ? FTP_PRIKEY : (!empty($submitted_form['private_key']) ? $submitted_form['private_key'] : '');
  1041 
  1604 
  1042 	// Sanitize the hostname, Some people might pass in odd-data:
  1605 	// Sanitize the hostname, Some people might pass in odd-data:
  1043 	$credentials['hostname'] = preg_replace('|\w+://|', '', $credentials['hostname']); //Strip any schemes off
  1606 	$credentials['hostname'] = preg_replace('|\w+://|', '', $credentials['hostname']); //Strip any schemes off
  1044 
  1607 
  1045 	if ( strpos($credentials['hostname'], ':') ) {
  1608 	if ( strpos($credentials['hostname'], ':') ) {
  1052 
  1615 
  1053 	if ( ( defined( 'FTP_SSH' ) && FTP_SSH ) || ( defined( 'FS_METHOD' ) && 'ssh2' == FS_METHOD ) ) {
  1616 	if ( ( defined( 'FTP_SSH' ) && FTP_SSH ) || ( defined( 'FS_METHOD' ) && 'ssh2' == FS_METHOD ) ) {
  1054 		$credentials['connection_type'] = 'ssh';
  1617 		$credentials['connection_type'] = 'ssh';
  1055 	} elseif ( ( defined( 'FTP_SSL' ) && FTP_SSL ) && 'ftpext' == $type ) { //Only the FTP Extension understands SSL
  1618 	} elseif ( ( defined( 'FTP_SSL' ) && FTP_SSL ) && 'ftpext' == $type ) { //Only the FTP Extension understands SSL
  1056 		$credentials['connection_type'] = 'ftps';
  1619 		$credentials['connection_type'] = 'ftps';
  1057 	} elseif ( ! empty( $_POST['connection_type'] ) ) {
  1620 	} elseif ( ! empty( $submitted_form['connection_type'] ) ) {
  1058 		$credentials['connection_type'] = wp_unslash( $_POST['connection_type'] );
  1621 		$credentials['connection_type'] = $submitted_form['connection_type'];
  1059 	} elseif ( ! isset( $credentials['connection_type'] ) ) { //All else fails (And it's not defaulted to something else saved), Default to FTP
  1622 	} elseif ( ! isset( $credentials['connection_type'] ) ) { //All else fails (And it's not defaulted to something else saved), Default to FTP
  1060 		$credentials['connection_type'] = 'ftp';
  1623 		$credentials['connection_type'] = 'ftp';
  1061 	}
  1624 	}
  1062 	if ( ! $error &&
  1625 	if ( ! $error &&
  1063 			(
  1626 			(
  1067 		$stored_credentials = $credentials;
  1630 		$stored_credentials = $credentials;
  1068 		if ( !empty($stored_credentials['port']) ) //save port as part of hostname to simplify above code.
  1631 		if ( !empty($stored_credentials['port']) ) //save port as part of hostname to simplify above code.
  1069 			$stored_credentials['hostname'] .= ':' . $stored_credentials['port'];
  1632 			$stored_credentials['hostname'] .= ':' . $stored_credentials['port'];
  1070 
  1633 
  1071 		unset($stored_credentials['password'], $stored_credentials['port'], $stored_credentials['private_key'], $stored_credentials['public_key']);
  1634 		unset($stored_credentials['password'], $stored_credentials['port'], $stored_credentials['private_key'], $stored_credentials['public_key']);
  1072 		if ( ! defined( 'WP_INSTALLING' ) ) {
  1635 		if ( ! wp_installing() ) {
  1073 			update_option( 'ftp_credentials', $stored_credentials );
  1636 			update_option( 'ftp_credentials', $stored_credentials );
  1074 		}
  1637 		}
  1075 		return $credentials;
  1638 		return $credentials;
  1076 	}
  1639 	}
  1077 	$hostname = isset( $credentials['hostname'] ) ? $credentials['hostname'] : '';
  1640 	$hostname = isset( $credentials['hostname'] ) ? $credentials['hostname'] : '';
  1095 		$types[ 'ftps' ] = __('FTPS (SSL)');
  1658 		$types[ 'ftps' ] = __('FTPS (SSL)');
  1096 	if ( extension_loaded('ssh2') && function_exists('stream_get_contents') )
  1659 	if ( extension_loaded('ssh2') && function_exists('stream_get_contents') )
  1097 		$types[ 'ssh' ] = __('SSH2');
  1660 		$types[ 'ssh' ] = __('SSH2');
  1098 
  1661 
  1099 	/**
  1662 	/**
  1100 	 * Filter the connection types to output to the filesystem credentials form.
  1663 	 * Filters the connection types to output to the filesystem credentials form.
  1101 	 *
  1664 	 *
  1102 	 * @since 2.9.0
  1665 	 * @since 2.9.0
       
  1666 	 * @since 4.6.0 The `$context` parameter default changed from `false` to an empty string.
  1103 	 *
  1667 	 *
  1104 	 * @param array  $types       Types of connections.
  1668 	 * @param array  $types       Types of connections.
  1105 	 * @param array  $credentials Credentials to connect with.
  1669 	 * @param array  $credentials Credentials to connect with.
  1106 	 * @param string $type        Chosen filesystem method.
  1670 	 * @param string $type        Chosen filesystem method.
  1107 	 * @param object $error       Error object.
  1671 	 * @param object $error       Error object.
  1109 	 *                            for being writable.
  1673 	 *                            for being writable.
  1110 	 */
  1674 	 */
  1111 	$types = apply_filters( 'fs_ftp_connection_types', $types, $credentials, $type, $error, $context );
  1675 	$types = apply_filters( 'fs_ftp_connection_types', $types, $credentials, $type, $error, $context );
  1112 
  1676 
  1113 ?>
  1677 ?>
  1114 <script type="text/javascript">
       
  1115 <!--
       
  1116 jQuery(function($){
       
  1117 	jQuery("#ssh").click(function () {
       
  1118 		jQuery("#ssh_keys").show();
       
  1119 	});
       
  1120 	jQuery("#ftp, #ftps").click(function () {
       
  1121 		jQuery("#ssh_keys").hide();
       
  1122 	});
       
  1123 	jQuery('#request-filesystem-credentials-form input[value=""]:first').focus();
       
  1124 });
       
  1125 -->
       
  1126 </script>
       
  1127 <form action="<?php echo esc_url( $form_post ) ?>" method="post">
  1678 <form action="<?php echo esc_url( $form_post ) ?>" method="post">
  1128 <div id="request-filesystem-credentials-form" class="request-filesystem-credentials-form">
  1679 <div id="request-filesystem-credentials-form" class="request-filesystem-credentials-form">
  1129 <h3 id="request-filesystem-credentials-title"><?php _e( 'Connection Information' ) ?></h3>
  1680 <?php
       
  1681 // Print a H1 heading in the FTP credentials modal dialog, default is a H2.
       
  1682 $heading_tag = 'h2';
       
  1683 if ( 'plugins.php' === $pagenow || 'plugin-install.php' === $pagenow ) {
       
  1684 	$heading_tag = 'h1';
       
  1685 }
       
  1686 echo "<$heading_tag id='request-filesystem-credentials-title'>" . __( 'Connection Information' ) . "</$heading_tag>";
       
  1687 ?>
  1130 <p id="request-filesystem-credentials-desc"><?php
  1688 <p id="request-filesystem-credentials-desc"><?php
  1131 	$label_user = __('Username');
  1689 	$label_user = __('Username');
  1132 	$label_pass = __('Password');
  1690 	$label_pass = __('Password');
  1133 	_e('To perform the requested action, WordPress needs to access your web server.');
  1691 	_e('To perform the requested action, WordPress needs to access your web server.');
  1134 	echo ' ';
  1692 	echo ' ';
  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 }