Changeset 43012 for trunk/src/wp-admin/includes/file.php
- Timestamp:
- 04/27/2018 07:53:37 PM (7 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-admin/includes/file.php
r42983 r43012 1935 1935 <?php 1936 1936 } 1937 1938 /** 1939 * Generate a single group for the personal data export report. 1940 * 1941 * @since 4.9.6 1942 * 1943 * @param array $group_data { 1944 * The group data to render. 1945 * 1946 * @type string $group_label The user-facing heading for the group, e.g. 'Comments'. 1947 * @type array $items { 1948 * An array of group items. 1949 * 1950 * @type array $group_item_data { 1951 * An array of name-value pairs for the item. 1952 * 1953 * @type string $name The user-facing name of an item name-value pair, e.g. 'IP Address'. 1954 * @type string $value The user-facing value of an item data pair, e.g. '50.60.70.0'. 1955 * } 1956 * } 1957 * } 1958 * @return string The HTML for this group and its items. 1959 */ 1960 function wp_privacy_generate_personal_data_export_group_html( $group_data ) { 1961 $allowed_tags = array( 1962 'a' => array( 1963 'href' => array(), 1964 'target' => array() 1965 ), 1966 'br' => array() 1967 ); 1968 $allowed_protocols = array( 'http', 'https' ); 1969 $group_html = ''; 1970 1971 $group_html .= '<h2>' . esc_html( $group_data['group_label'] ) . '</h2>'; 1972 $group_html .= '<div>'; 1973 1974 foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) { 1975 $group_html .= '<table>'; 1976 $group_html .= '<tbody>'; 1977 1978 foreach ( (array) $group_item_data as $group_item_datum ) { 1979 $group_html .= '<tr>'; 1980 $group_html .= '<th>' . esc_html( $group_item_datum['name'] ) . '</th>'; 1981 $group_html .= '<td>' . wp_kses( $group_item_datum['value'], $allowed_tags, $allowed_protocols ) . '</td>'; 1982 $group_html .= '</tr>'; 1983 } 1984 1985 $group_html .= '</tbody>'; 1986 $group_html .= '</table>'; 1987 } 1988 1989 $group_html .= '</div>'; 1990 1991 return $group_html; 1992 } 1993 1994 /** 1995 * Generate the personal data export file. 1996 * 1997 * @since 4.9.6 1998 * 1999 * @param int $request_id The export request ID. 2000 */ 2001 function wp_privacy_generate_personal_data_export_file( $request_id ) { 2002 // Maybe make this a cron job instead. 2003 wp_privacy_delete_old_export_files(); 2004 2005 if ( ! class_exists( 'ZipArchive' ) ) { 2006 wp_send_json_error( __( 'Unable to generate export file. ZipArchive not available.' ) ); 2007 } 2008 2009 // Get the request data. 2010 $request = wp_get_user_request_data( $request_id ); 2011 2012 if ( ! $request || 'export_personal_data' !== $request->action_name ) { 2013 wp_send_json_error( __( 'Invalid request ID when generating export file' ) ); 2014 } 2015 2016 $email_address = $request->email; 2017 2018 if ( ! is_email( $email_address ) ) { 2019 wp_send_json_error( __( 'Invalid email address when generating export file' ) ); 2020 } 2021 2022 // Create the exports folder if needed. 2023 $upload_dir = wp_upload_dir(); 2024 $exports_dir = trailingslashit( $upload_dir['basedir'] . '/exports' ); 2025 $exports_url = trailingslashit( $upload_dir['baseurl'] . '/exports' ); 2026 2027 $result = wp_mkdir_p( $exports_dir ); 2028 if ( is_wp_error( $result ) ) { 2029 wp_send_json_error( $result->get_error_message() ); 2030 } 2031 2032 // Protect export folder from browsing. 2033 $index_pathname = $exports_dir . 'index.html'; 2034 if ( ! file_exists( $index_pathname ) ) { 2035 $file = fopen( $index_pathname, 'w' ); 2036 if ( false === $file ) { 2037 wp_send_json_error( __( 'Unable to protect export folder from browsing' ) ); 2038 } 2039 fwrite( $file, 'Silence is golden.' ); 2040 fclose( $file ); 2041 } 2042 2043 $stripped_email = str_replace( '@', '-at-', $email_address ); 2044 $stripped_email = sanitize_title( $stripped_email ); // slugify the email address 2045 $obscura = md5( rand() ); 2046 $file_basename = 'wp-personal-data-file-' . $stripped_email . '-' . $obscura; 2047 $html_report_filename = $file_basename . '.html'; 2048 $html_report_pathname = $exports_dir . $html_report_filename; 2049 $file = fopen( $html_report_pathname, 'w' ); 2050 if ( false === $file ) { 2051 wp_send_json_error( __( 'Unable to open export file (HTML report) for writing' ) ); 2052 } 2053 2054 $title = sprintf( 2055 // translators: %s Users e-mail address. 2056 __( 'Personal Data Export for %s' ), 2057 $email_address 2058 ); 2059 2060 // Open HTML. 2061 fwrite( $file, "<!DOCTYPE html>\n" ); 2062 fwrite( $file, "<html>\n" ); 2063 2064 // Head. 2065 fwrite( $file, "<head>\n" ); 2066 fwrite( $file, "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />\n" ); 2067 fwrite( $file, "<style type='text/css'>" ); 2068 fwrite( $file, "body { color: black; font-family: Arial, sans-serif; font-size: 11pt; margin: 15px auto; width: 860px; }" ); 2069 fwrite( $file, "table { background: #f0f0f0; border: 1px solid #ddd; margin-bottom: 20px; width: 100%; }" ); 2070 fwrite( $file, "th { padding: 5px; text-align: left; width: 20%; }" ); 2071 fwrite( $file, "td { padding: 5px; }" ); 2072 fwrite( $file, "tr:nth-child(odd) { background-color: #fafafa; }" ); 2073 fwrite( $file, "</style>" ); 2074 fwrite( $file, "<title>" ); 2075 fwrite( $file, esc_html( $title ) ); 2076 fwrite( $file, "</title>" ); 2077 fwrite( $file, "</head>\n" ); 2078 2079 // Body. 2080 fwrite( $file, "<body>\n" ); 2081 2082 // Heading. 2083 fwrite( $file, "<h1>" . esc_html__( 'Personal Data Export' ) . "</h1>" ); 2084 2085 // And now, all the Groups. 2086 $groups = get_post_meta( $request_id, '_export_data_grouped', true ); 2087 2088 // First, build an "About" group on the fly for this report. 2089 $about_group = array( 2090 'group_label' => __( 'About' ), 2091 'items' => array( 2092 'about-1' => array( 2093 array( 2094 'name' => __( 'Report generated for' ), 2095 'value' => $email_address, 2096 ), 2097 array( 2098 'name' => __( 'For site' ), 2099 'value' => get_bloginfo( 'name' ), 2100 ), 2101 array( 2102 'name' => __( 'At URL' ), 2103 'value' => get_bloginfo( 'url' ), 2104 ), 2105 array( 2106 'name' => __( 'On' ), 2107 'value' => current_time( 'mysql' ), 2108 ), 2109 ), 2110 ), 2111 ); 2112 2113 // Merge in the special about group. 2114 $groups = array_merge( array( 'about' => $about_group ), $groups ); 2115 2116 // Now, iterate over every group in $groups and have the formatter render it in HTML. 2117 foreach ( (array) $groups as $group_id => $group_data ) { 2118 fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data ) ); 2119 } 2120 2121 fwrite( $file, "</body>\n" ); 2122 2123 // Close HTML. 2124 fwrite( $file, "</html>\n" ); 2125 fclose( $file ); 2126 2127 // Now, generate the ZIP. 2128 $archive_filename = $file_basename . '.zip'; 2129 $archive_pathname = $exports_dir . $archive_filename; 2130 $archive_url = $exports_url . $archive_filename; 2131 2132 $zip = new ZipArchive; 2133 2134 if ( TRUE === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) { 2135 $zip->addFile( $html_report_pathname, 'index.html' ); 2136 $zip->close(); 2137 } else { 2138 wp_send_json_error( __( 'Unable to open export file (archive) for writing' ) ); 2139 } 2140 2141 // And remove the HTML file. 2142 unlink( $html_report_pathname ); 2143 2144 // Save the export file in the request. 2145 update_post_meta( $request_id, '_export_file_url', $archive_url ); 2146 update_post_meta( $request_id, '_export_file_path', $archive_pathname ); 2147 } 2148 2149 /** 2150 * Send an email to the user with a link to the personal data export file 2151 * 2152 * @since 4.9.6 2153 * 2154 * @param int $request_id The request ID for this personal data export. 2155 * @return true|WP_Error True on success or `WP_Error` on failure. 2156 */ 2157 function wp_privacy_send_personal_data_export_email( $request_id ) { 2158 // Get the request data. 2159 $request = wp_get_user_request_data( $request_id ); 2160 2161 if ( ! $request || 'export_personal_data' !== $request->action_name ) { 2162 return new WP_Error( 'invalid', __( 'Invalid request ID when sending personal data export email.' ) ); 2163 } 2164 2165 /* translators: Do not translate LINK, EMAIL, SITENAME, SITEURL: those are placeholders. */ 2166 $email_text = __( 2167 'Howdy, 2168 2169 Your request for an export of personal data has been completed. You may 2170 download your personal data by clicking on the link below. This link is 2171 good for the next 3 days. 2172 2173 ###LINK### 2174 2175 This email has been sent to ###EMAIL###. 2176 2177 Regards, 2178 All at ###SITENAME### 2179 ###SITEURL###' 2180 ); 2181 2182 /** 2183 * Filters the text of the email sent with a personal data export file. 2184 * 2185 * The following strings have a special meaning and will get replaced dynamically: 2186 * ###LINK### URL of the personal data export file for the user. 2187 * ###EMAIL### The email we are sending to. 2188 * ###SITENAME### The name of the site. 2189 * ###SITEURL### The URL to the site. 2190 * 2191 * @since 4.9.6 2192 * 2193 * @param string $email_text Text in the email. 2194 * @param int $request_id The request ID for this personal data export. 2195 */ 2196 $content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id ); 2197 2198 $email_address = $request->email; 2199 $export_file_url = get_post_meta( $request_id, '_export_file_url', true ); 2200 $site_name = is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' ); 2201 $site_url = network_home_url(); 2202 2203 $content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content ); 2204 $content = str_replace( '###EMAIL###', $email_address, $content ); 2205 $content = str_replace( '###SITENAME###', wp_specialchars_decode( $site_name, ENT_QUOTES ), $content ); 2206 $content = str_replace( '###SITEURL###', esc_url_raw( $site_url ), $content ); 2207 2208 $mail_success = wp_mail( 2209 $email_address, 2210 sprintf( 2211 __( '[%s] Personal Data Export' ), 2212 wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) 2213 ), 2214 $content 2215 ); 2216 2217 if ( ! $mail_success ) { 2218 return new WP_Error( 'error', __( 'Unable to send personal data export email.' ) ); 2219 } 2220 2221 return true; 2222 } 2223 2224 /** 2225 * Intercept personal data exporter page ajax responses in order to assemble the personal data export file. 2226 * @see wp_privacy_personal_data_export_page 2227 * @since 4.9.6 2228 * 2229 * @param array $response The response from the personal data exporter for the given page. 2230 * @param int $exporter_index The index of the personal data exporter. Begins at 1. 2231 * @param string $email_address The email address of the user whose personal data this is. 2232 * @param int $page The page of personal data for this exporter. Begins at 1. 2233 * @param int $request_id The request ID for this personal data export. 2234 * @param bool $send_as_email Whether the final results of the export should be emailed to the user. 2235 * @return array The filtered response. 2236 */ 2237 function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email ) { 2238 /* Do some simple checks on the shape of the response from the exporter. 2239 * If the exporter response is malformed, don't attempt to consume it - let it 2240 * pass through to generate a warning to the user by default ajax processing. 2241 */ 2242 if ( ! is_array( $response ) ) { 2243 return $response; 2244 } 2245 2246 if ( ! array_key_exists( 'done', $response ) ) { 2247 return $response; 2248 } 2249 2250 if ( ! array_key_exists( 'data', $response ) ) { 2251 return $response; 2252 } 2253 2254 if ( ! is_array( $response['data'] ) ) { 2255 return $response; 2256 } 2257 2258 // Get the request data. 2259 $request = wp_get_user_request_data( $request_id ); 2260 2261 if ( ! $request || 'export_personal_data' !== $request->action_name ) { 2262 wp_send_json_error( __( 'Invalid request ID when merging exporter data' ) ); 2263 } 2264 2265 $export_data = array(); 2266 2267 // First exporter, first page? Reset the report data accumulation array. 2268 if ( 1 === $exporter_index && 1 === $page ) { 2269 update_post_meta( $request_id, '_export_data_raw', $export_data ); 2270 } else { 2271 $export_data = get_post_meta( $request_id, '_export_data_raw', true ); 2272 } 2273 2274 // Now, merge the data from the exporter response into the data we have accumulated already. 2275 $export_data = array_merge( $export_data, $response['data'] ); 2276 update_post_meta( $request_id, '_export_data_raw', $export_data ); 2277 2278 // If we are not yet on the last page of the last exporter, return now. 2279 $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() ); 2280 $is_last_exporter = $exporter_index === count( $exporters ); 2281 $exporter_done = $response['done']; 2282 if ( ! $is_last_exporter || ! $exporter_done ) { 2283 return $response; 2284 } 2285 2286 // Last exporter, last page - let's prepare the export file. 2287 2288 // First we need to re-organize the raw data hierarchically in groups and items. 2289 $groups = array(); 2290 foreach ( (array) $export_data as $export_datum ) { 2291 $group_id = $export_datum['group_id']; 2292 $group_label = $export_datum['group_label']; 2293 if ( ! array_key_exists( $group_id, $groups ) ) { 2294 $groups[ $group_id ] = array( 2295 'group_label' => $group_label, 2296 'items' => array(), 2297 ); 2298 } 2299 2300 $item_id = $export_datum['item_id']; 2301 if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) { 2302 $groups[ $group_id ]['items'][ $item_id ] = array(); 2303 } 2304 2305 $old_item_data = $groups[ $group_id ]['items'][ $item_id ]; 2306 $merged_item_data = array_merge( $export_datum['data'], $old_item_data ); 2307 $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data; 2308 } 2309 2310 // Then save the grouped data into the request. 2311 delete_post_meta( $request_id, '_export_data_raw' ); 2312 update_post_meta( $request_id, '_export_data_grouped', $groups ); 2313 2314 // And now, generate the export file, cleaning up any previous file 2315 $export_path = get_post_meta( $request_id, '_export_file_path', true ); 2316 if ( ! empty( $export_path ) ) { 2317 delete_post_meta( $request_id, '_export_file_path' ); 2318 @unlink( $export_path ); 2319 } 2320 delete_post_meta( $request_id, '_export_file_url' ); 2321 2322 // Generate the export file from the collected, grouped personal data. 2323 do_action( 'wp_privacy_personal_data_export_file', $request_id ); 2324 2325 // Clear the grouped data now that it is no longer needed. 2326 delete_post_meta( $request_id, '_export_data_grouped' ); 2327 2328 // If the destination is email, send it now. 2329 if ( $send_as_email ) { 2330 $mail_success = wp_privacy_send_personal_data_export_email( $request_id ); 2331 if ( is_wp_error( $mail_success ) ) { 2332 wp_send_json_error( $mail_success->get_error_message() ); 2333 } 2334 } else { 2335 // Modify the response to include the URL of the export file so the browser can fetch it. 2336 $export_file_url = get_post_meta( $request_id, '_export_file_url', true ); 2337 if ( ! empty( $export_file_url ) ) { 2338 $response['url'] = $export_file_url; 2339 } 2340 } 2341 2342 // Update the request to completed state. 2343 _wp_privacy_completed_request( $request_id ); 2344 2345 return $response; 2346 } 2347 2348 /** 2349 * Cleans up export files older than three days old. 2350 * 2351 * @since 4.9.6 2352 */ 2353 function wp_privacy_delete_old_export_files() { 2354 $upload_dir = wp_upload_dir(); 2355 $exports_dir = trailingslashit( $upload_dir['basedir'] . '/exports' ); 2356 $export_files = list_files( $exports_dir ); 2357 2358 foreach( (array) $export_files as $export_file ) { 2359 $file_age_in_seconds = time() - filemtime( $export_file ); 2360 2361 if ( 3 * DAY_IN_SECONDS < $file_age_in_seconds ) { 2362 @unlink( $export_file ); 2363 } 2364 } 2365 }
Note: See TracChangeset
for help on using the changeset viewer.