Changeset 43092 for branches/4.9/src/wp-admin/includes/file.php
- Timestamp:
- 05/02/2018 02:15:05 AM (7 years ago)
- Location:
- branches/4.9
- Files:
-
- 2 edited
Legend:
- Unmodified
- Added
- Removed
-
branches/4.9
- Property svn:mergeinfo changed
/trunk merged: 43012,43089
- Property svn:mergeinfo changed
-
branches/4.9/src/wp-admin/includes/file.php
r42811 r43092 1798 1798 <?php 1799 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 $group_html .= '<tr>'; 1843 $group_html .= '<th>' . esc_html( $group_item_datum['name'] ) . '</th>'; 1844 $group_html .= '<td>' . wp_kses( $group_item_datum['value'], $allowed_tags, $allowed_protocols ) . '</td>'; 1845 $group_html .= '</tr>'; 1846 } 1847 1848 $group_html .= '</tbody>'; 1849 $group_html .= '</table>'; 1850 } 1851 1852 $group_html .= '</div>'; 1853 1854 return $group_html; 1855 } 1856 1857 /** 1858 * Generate the personal data export file. 1859 * 1860 * @since 4.9.6 1861 * 1862 * @param int $request_id The export request ID. 1863 */ 1864 function wp_privacy_generate_personal_data_export_file( $request_id ) { 1865 // Maybe make this a cron job instead. 1866 wp_privacy_delete_old_export_files(); 1867 1868 if ( ! class_exists( 'ZipArchive' ) ) { 1869 wp_send_json_error( __( 'Unable to generate export file. ZipArchive not available.' ) ); 1870 } 1871 1872 // Get the request data. 1873 $request = wp_get_user_request_data( $request_id ); 1874 1875 if ( ! $request || 'export_personal_data' !== $request->action_name ) { 1876 wp_send_json_error( __( 'Invalid request ID when generating export file' ) ); 1877 } 1878 1879 $email_address = $request->email; 1880 1881 if ( ! is_email( $email_address ) ) { 1882 wp_send_json_error( __( 'Invalid email address when generating export file' ) ); 1883 } 1884 1885 // Create the exports folder if needed. 1886 $upload_dir = wp_upload_dir(); 1887 $exports_dir = trailingslashit( $upload_dir['basedir'] . '/exports' ); 1888 $exports_url = trailingslashit( $upload_dir['baseurl'] . '/exports' ); 1889 1890 $result = wp_mkdir_p( $exports_dir ); 1891 if ( is_wp_error( $result ) ) { 1892 wp_send_json_error( $result->get_error_message() ); 1893 } 1894 1895 // Protect export folder from browsing. 1896 $index_pathname = $exports_dir . 'index.html'; 1897 if ( ! file_exists( $index_pathname ) ) { 1898 $file = fopen( $index_pathname, 'w' ); 1899 if ( false === $file ) { 1900 wp_send_json_error( __( 'Unable to protect export folder from browsing' ) ); 1901 } 1902 fwrite( $file, 'Silence is golden.' ); 1903 fclose( $file ); 1904 } 1905 1906 $stripped_email = str_replace( '@', '-at-', $email_address ); 1907 $stripped_email = sanitize_title( $stripped_email ); // slugify the email address 1908 $obscura = md5( rand() ); 1909 $file_basename = 'wp-personal-data-file-' . $stripped_email . '-' . $obscura; 1910 $html_report_filename = $file_basename . '.html'; 1911 $html_report_pathname = $exports_dir . $html_report_filename; 1912 $file = fopen( $html_report_pathname, 'w' ); 1913 if ( false === $file ) { 1914 wp_send_json_error( __( 'Unable to open export file (HTML report) for writing' ) ); 1915 } 1916 1917 $title = sprintf( 1918 /* translators: %s: user's e-mail address */ 1919 __( 'Personal Data Export for %s' ), 1920 $email_address 1921 ); 1922 1923 // Open HTML. 1924 fwrite( $file, "<!DOCTYPE html>\n" ); 1925 fwrite( $file, "<html>\n" ); 1926 1927 // Head. 1928 fwrite( $file, "<head>\n" ); 1929 fwrite( $file, "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />\n" ); 1930 fwrite( $file, "<style type='text/css'>" ); 1931 fwrite( $file, "body { color: black; font-family: Arial, sans-serif; font-size: 11pt; margin: 15px auto; width: 860px; }" ); 1932 fwrite( $file, "table { background: #f0f0f0; border: 1px solid #ddd; margin-bottom: 20px; width: 100%; }" ); 1933 fwrite( $file, "th { padding: 5px; text-align: left; width: 20%; }" ); 1934 fwrite( $file, "td { padding: 5px; }" ); 1935 fwrite( $file, "tr:nth-child(odd) { background-color: #fafafa; }" ); 1936 fwrite( $file, "</style>" ); 1937 fwrite( $file, "<title>" ); 1938 fwrite( $file, esc_html( $title ) ); 1939 fwrite( $file, "</title>" ); 1940 fwrite( $file, "</head>\n" ); 1941 1942 // Body. 1943 fwrite( $file, "<body>\n" ); 1944 1945 // Heading. 1946 fwrite( $file, "<h1>" . esc_html__( 'Personal Data Export' ) . "</h1>" ); 1947 1948 // And now, all the Groups. 1949 $groups = get_post_meta( $request_id, '_export_data_grouped', true ); 1950 1951 // First, build an "About" group on the fly for this report. 1952 $about_group = array( 1953 'group_label' => __( 'About' ), 1954 'items' => array( 1955 'about-1' => array( 1956 array( 1957 'name' => __( 'Report generated for' ), 1958 'value' => $email_address, 1959 ), 1960 array( 1961 'name' => __( 'For site' ), 1962 'value' => get_bloginfo( 'name' ), 1963 ), 1964 array( 1965 'name' => __( 'At URL' ), 1966 'value' => get_bloginfo( 'url' ), 1967 ), 1968 array( 1969 'name' => __( 'On' ), 1970 'value' => current_time( 'mysql' ), 1971 ), 1972 ), 1973 ), 1974 ); 1975 1976 // Merge in the special about group. 1977 $groups = array_merge( array( 'about' => $about_group ), $groups ); 1978 1979 // Now, iterate over every group in $groups and have the formatter render it in HTML. 1980 foreach ( (array) $groups as $group_id => $group_data ) { 1981 fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data ) ); 1982 } 1983 1984 fwrite( $file, "</body>\n" ); 1985 1986 // Close HTML. 1987 fwrite( $file, "</html>\n" ); 1988 fclose( $file ); 1989 1990 // Now, generate the ZIP. 1991 $archive_filename = $file_basename . '.zip'; 1992 $archive_pathname = $exports_dir . $archive_filename; 1993 $archive_url = $exports_url . $archive_filename; 1994 1995 $zip = new ZipArchive; 1996 1997 if ( TRUE === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) { 1998 $zip->addFile( $html_report_pathname, 'index.html' ); 1999 $zip->close(); 2000 } else { 2001 wp_send_json_error( __( 'Unable to open export file (archive) for writing' ) ); 2002 } 2003 2004 // And remove the HTML file. 2005 unlink( $html_report_pathname ); 2006 2007 // Save the export file in the request. 2008 update_post_meta( $request_id, '_export_file_url', $archive_url ); 2009 update_post_meta( $request_id, '_export_file_path', $archive_pathname ); 2010 } 2011 2012 /** 2013 * Send an email to the user with a link to the personal data export file 2014 * 2015 * @since 4.9.6 2016 * 2017 * @param int $request_id The request ID for this personal data export. 2018 * @return true|WP_Error True on success or `WP_Error` on failure. 2019 */ 2020 function wp_privacy_send_personal_data_export_email( $request_id ) { 2021 // Get the request data. 2022 $request = wp_get_user_request_data( $request_id ); 2023 2024 if ( ! $request || 'export_personal_data' !== $request->action_name ) { 2025 return new WP_Error( 'invalid', __( 'Invalid request ID when sending personal data export email.' ) ); 2026 } 2027 2028 /* translators: Do not translate LINK, EMAIL, SITENAME, SITEURL: those are placeholders. */ 2029 $email_text = __( 2030 'Howdy, 2031 2032 Your request for an export of personal data has been completed. You may 2033 download your personal data by clicking on the link below. This link is 2034 good for the next 3 days. 2035 2036 ###LINK### 2037 2038 This email has been sent to ###EMAIL###. 2039 2040 Regards, 2041 All at ###SITENAME### 2042 ###SITEURL###' 2043 ); 2044 2045 /** 2046 * Filters the text of the email sent with a personal data export file. 2047 * 2048 * The following strings have a special meaning and will get replaced dynamically: 2049 * ###LINK### URL of the personal data export file for the user. 2050 * ###EMAIL### The email we are sending to. 2051 * ###SITENAME### The name of the site. 2052 * ###SITEURL### The URL to the site. 2053 * 2054 * @since 4.9.6 2055 * 2056 * @param string $email_text Text in the email. 2057 * @param int $request_id The request ID for this personal data export. 2058 */ 2059 $content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id ); 2060 2061 $email_address = $request->email; 2062 $export_file_url = get_post_meta( $request_id, '_export_file_url', true ); 2063 $site_name = is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' ); 2064 $site_url = network_home_url(); 2065 2066 $content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content ); 2067 $content = str_replace( '###EMAIL###', $email_address, $content ); 2068 $content = str_replace( '###SITENAME###', wp_specialchars_decode( $site_name, ENT_QUOTES ), $content ); 2069 $content = str_replace( '###SITEURL###', esc_url_raw( $site_url ), $content ); 2070 2071 $mail_success = wp_mail( 2072 $email_address, 2073 sprintf( 2074 __( '[%s] Personal Data Export' ), 2075 wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) 2076 ), 2077 $content 2078 ); 2079 2080 if ( ! $mail_success ) { 2081 return new WP_Error( 'error', __( 'Unable to send personal data export email.' ) ); 2082 } 2083 2084 return true; 2085 } 2086 2087 /** 2088 * Intercept personal data exporter page ajax responses in order to assemble the personal data export file. 2089 * @see wp_privacy_personal_data_export_page 2090 * @since 4.9.6 2091 * 2092 * @param array $response The response from the personal data exporter for the given page. 2093 * @param int $exporter_index The index of the personal data exporter. Begins at 1. 2094 * @param string $email_address The email address of the user whose personal data this is. 2095 * @param int $page The page of personal data for this exporter. Begins at 1. 2096 * @param int $request_id The request ID for this personal data export. 2097 * @param bool $send_as_email Whether the final results of the export should be emailed to the user. 2098 * @return array The filtered response. 2099 */ 2100 function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email ) { 2101 /* Do some simple checks on the shape of the response from the exporter. 2102 * If the exporter response is malformed, don't attempt to consume it - let it 2103 * pass through to generate a warning to the user by default ajax processing. 2104 */ 2105 if ( ! is_array( $response ) ) { 2106 return $response; 2107 } 2108 2109 if ( ! array_key_exists( 'done', $response ) ) { 2110 return $response; 2111 } 2112 2113 if ( ! array_key_exists( 'data', $response ) ) { 2114 return $response; 2115 } 2116 2117 if ( ! is_array( $response['data'] ) ) { 2118 return $response; 2119 } 2120 2121 // Get the request data. 2122 $request = wp_get_user_request_data( $request_id ); 2123 2124 if ( ! $request || 'export_personal_data' !== $request->action_name ) { 2125 wp_send_json_error( __( 'Invalid request ID when merging exporter data' ) ); 2126 } 2127 2128 $export_data = array(); 2129 2130 // First exporter, first page? Reset the report data accumulation array. 2131 if ( 1 === $exporter_index && 1 === $page ) { 2132 update_post_meta( $request_id, '_export_data_raw', $export_data ); 2133 } else { 2134 $export_data = get_post_meta( $request_id, '_export_data_raw', true ); 2135 } 2136 2137 // Now, merge the data from the exporter response into the data we have accumulated already. 2138 $export_data = array_merge( $export_data, $response['data'] ); 2139 update_post_meta( $request_id, '_export_data_raw', $export_data ); 2140 2141 // If we are not yet on the last page of the last exporter, return now. 2142 $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() ); 2143 $is_last_exporter = $exporter_index === count( $exporters ); 2144 $exporter_done = $response['done']; 2145 if ( ! $is_last_exporter || ! $exporter_done ) { 2146 return $response; 2147 } 2148 2149 // Last exporter, last page - let's prepare the export file. 2150 2151 // First we need to re-organize the raw data hierarchically in groups and items. 2152 $groups = array(); 2153 foreach ( (array) $export_data as $export_datum ) { 2154 $group_id = $export_datum['group_id']; 2155 $group_label = $export_datum['group_label']; 2156 if ( ! array_key_exists( $group_id, $groups ) ) { 2157 $groups[ $group_id ] = array( 2158 'group_label' => $group_label, 2159 'items' => array(), 2160 ); 2161 } 2162 2163 $item_id = $export_datum['item_id']; 2164 if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) { 2165 $groups[ $group_id ]['items'][ $item_id ] = array(); 2166 } 2167 2168 $old_item_data = $groups[ $group_id ]['items'][ $item_id ]; 2169 $merged_item_data = array_merge( $export_datum['data'], $old_item_data ); 2170 $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data; 2171 } 2172 2173 // Then save the grouped data into the request. 2174 delete_post_meta( $request_id, '_export_data_raw' ); 2175 update_post_meta( $request_id, '_export_data_grouped', $groups ); 2176 2177 // And now, generate the export file, cleaning up any previous file 2178 $export_path = get_post_meta( $request_id, '_export_file_path', true ); 2179 if ( ! empty( $export_path ) ) { 2180 delete_post_meta( $request_id, '_export_file_path' ); 2181 @unlink( $export_path ); 2182 } 2183 delete_post_meta( $request_id, '_export_file_url' ); 2184 2185 // Generate the export file from the collected, grouped personal data. 2186 do_action( 'wp_privacy_personal_data_export_file', $request_id ); 2187 2188 // Clear the grouped data now that it is no longer needed. 2189 delete_post_meta( $request_id, '_export_data_grouped' ); 2190 2191 // If the destination is email, send it now. 2192 if ( $send_as_email ) { 2193 $mail_success = wp_privacy_send_personal_data_export_email( $request_id ); 2194 if ( is_wp_error( $mail_success ) ) { 2195 wp_send_json_error( $mail_success->get_error_message() ); 2196 } 2197 } else { 2198 // Modify the response to include the URL of the export file so the browser can fetch it. 2199 $export_file_url = get_post_meta( $request_id, '_export_file_url', true ); 2200 if ( ! empty( $export_file_url ) ) { 2201 $response['url'] = $export_file_url; 2202 } 2203 } 2204 2205 // Update the request to completed state. 2206 _wp_privacy_completed_request( $request_id ); 2207 2208 return $response; 2209 } 2210 2211 /** 2212 * Cleans up export files older than three days old. 2213 * 2214 * @since 4.9.6 2215 */ 2216 function wp_privacy_delete_old_export_files() { 2217 $upload_dir = wp_upload_dir(); 2218 $exports_dir = trailingslashit( $upload_dir['basedir'] . '/exports' ); 2219 $export_files = list_files( $exports_dir ); 2220 2221 foreach( (array) $export_files as $export_file ) { 2222 $file_age_in_seconds = time() - filemtime( $export_file ); 2223 2224 if ( 3 * DAY_IN_SECONDS < $file_age_in_seconds ) { 2225 @unlink( $export_file ); 2226 } 2227 } 2228 }
Note: See TracChangeset
for help on using the changeset viewer.