| 1937 | |
| 1938 | // TODO phpDocs |
| 1939 | function wp_privacy_generate_personal_data_export_group_html( $group_id, $group_data ) { |
| 1940 | $group_html = ''; |
| 1941 | |
| 1942 | $group_html .= '<h2>' . esc_html( $group_data['group_label'] ) . '</h2>'; |
| 1943 | $group_html .= '<div>'; |
| 1944 | |
| 1945 | foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) { |
| 1946 | $group_html .= '<table>'; |
| 1947 | $group_html .= '<tbody>'; |
| 1948 | |
| 1949 | foreach ( (array) $group_item_data as $group_item_datum ) { |
| 1950 | $group_html .= '<tr>'; |
| 1951 | $group_html .= '<th>' . esc_html( $group_item_datum['name'] ) . '</th>'; |
| 1952 | // TODO entities! |
| 1953 | $group_html .= '<td>' . esc_html( $group_item_datum['value'] ) . '</td>'; |
| 1954 | // TODO support attachment links |
| 1955 | $group_html .= '</tr>'; |
| 1956 | } |
| 1957 | |
| 1958 | $group_html .= '</tbody>'; |
| 1959 | $group_html .= '</table>'; |
| 1960 | } |
| 1961 | |
| 1962 | $group_html .= '</div>'; |
| 1963 | |
| 1964 | return $group_html; |
| 1965 | } |
| 1966 | |
| 1967 | // TODO phpDocs |
| 1968 | function wp_privacy_generate_personal_data_export( $exports_dir, $email_address, $personal_data ) { |
| 1969 | |
| 1970 | // TODO Instead of HTML, return a ZIP containing the HTML and the attachments |
| 1971 | $timestamp = current_time( 'timestamp' ); |
| 1972 | $index_filename = 'wp-personal-data-export-' . md5( $email_address ) . '-' . md5( $timestamp ) . '.html'; |
| 1973 | |
| 1974 | $index_path = trailingslashit( $exports_dir ) . $index_filename; |
| 1975 | $file = fopen( $index_path, 'w' ); |
| 1976 | // TODO catch fopen error |
| 1977 | |
| 1978 | $title = sprintf( |
| 1979 | __( 'Personal Data Export for %s' ), |
| 1980 | $email_address |
| 1981 | ); |
| 1982 | |
| 1983 | // Open HTML |
| 1984 | fwrite( $file, "<!DOCTYPE html>\n" ); |
| 1985 | fwrite( $file, "<html>\n" ); |
| 1986 | |
| 1987 | // Head |
| 1988 | fwrite( $file, "<head>\n" ); |
| 1989 | fwrite( $file, "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />\n" ); |
| 1990 | fwrite( $file, "<style type='text/css'>" ); |
| 1991 | fwrite( $file, "body { color: black; font-family: Arial, sans-serif; font-size: 11pt; margin: 15px auto; width: 860px; }" ); |
| 1992 | fwrite( $file, "table { background: #f0f0f0; border: 1px solid #ddd; margin-bottom: 20px; width: 100%; }" ); |
| 1993 | fwrite( $file, "th { padding: 5px; text-align: left; width: 20%; }" ); |
| 1994 | fwrite( $file, "td { padding: 5px; }" ); |
| 1995 | fwrite( $file, "tr:nth-child(odd) { background-color: #fafafa; }" ); |
| 1996 | fwrite( $file, "</style>" ); |
| 1997 | fwrite( $file, "<title>" ); |
| 1998 | fwrite( $file, esc_html( $title ) ); |
| 1999 | fwrite( $file, "</title>" ); |
| 2000 | fwrite( $file, "</head>\n" ); |
| 2001 | |
| 2002 | // Body |
| 2003 | fwrite( $file, "<body>\n" ); |
| 2004 | |
| 2005 | // Heading |
| 2006 | fwrite( $file, "<h1>" . esc_html__( 'Personal Data Export' ) . "</h1>" ); |
| 2007 | |
| 2008 | // And now, all the Groups |
| 2009 | $groups = array(); |
| 2010 | |
| 2011 | // First, build a "About" group on the fly for this report |
| 2012 | $groups['about'] = array( |
| 2013 | 'group_label' => __( 'About' ), |
| 2014 | 'items' => array( |
| 2015 | 'about-1' => array( |
| 2016 | array( |
| 2017 | 'name' => __( 'Report generated for' ), |
| 2018 | 'value' => $email_address, |
| 2019 | ), |
| 2020 | array( |
| 2021 | 'name' => __( 'For site' ), |
| 2022 | 'value' => get_bloginfo( 'name' ), |
| 2023 | ), |
| 2024 | array( |
| 2025 | 'name' => __( 'At URL' ), |
| 2026 | 'value' => get_bloginfo( 'url' ), |
| 2027 | ), |
| 2028 | array( |
| 2029 | 'name' => __( 'On' ), |
| 2030 | 'value' => current_time( 'mysql' ), |
| 2031 | ), |
| 2032 | ), |
| 2033 | ) |
| 2034 | ); |
| 2035 | |
| 2036 | // Next, iterate over every item in $personal_data |
| 2037 | // Extract all the unique group_ids, group_labels and item_ids |
| 2038 | // Initialize/append the data for each item under its item_id |
| 2039 | foreach ( (array) $personal_data as $personal_datum ) { |
| 2040 | $group_id = $personal_datum['group_id']; |
| 2041 | $group_label = $personal_datum['group_label']; |
| 2042 | if ( ! array_key_exists( $group_id, $groups ) ) { |
| 2043 | $groups[ $group_id ] = array( |
| 2044 | 'group_label' => $group_label, |
| 2045 | 'items' => array(), |
| 2046 | ); |
| 2047 | } |
| 2048 | |
| 2049 | $item_id = $personal_datum['item_id']; |
| 2050 | if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) { |
| 2051 | $groups[ $group_id ]['items'][ $item_id ] = array(); |
| 2052 | } |
| 2053 | |
| 2054 | $old_item_data = $groups[ $group_id ]['items'][ $item_id ]; |
| 2055 | $merged_item_data = array_merge( $personal_datum['data'], $old_item_data ); |
| 2056 | $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data; |
| 2057 | } |
| 2058 | |
| 2059 | // Now, iterate over every group in $groups and have the formatter render it in HTML |
| 2060 | foreach ( (array) $groups as $group_id => $group_data ) { |
| 2061 | fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_id, $group_data ) ); |
| 2062 | } |
| 2063 | |
| 2064 | fwrite( $file, "</body>\n" ); |
| 2065 | |
| 2066 | // Close HTML |
| 2067 | fwrite( $file, "</html>\n" ); |
| 2068 | fclose( $file ); |
| 2069 | |
| 2070 | // Now, generate the ZIP |
| 2071 | $archive_filename = 'wp-personal-data-export-' . md5( $email_address ) . '-' . md5( $timestamp ) . '.zip'; |
| 2072 | $archive_path = trailingslashit( $exports_dir ) . $archive_filename; |
| 2073 | |
| 2074 | $zip = new ZipArchive; |
| 2075 | // TODO test for no ZipArchive to work with |
| 2076 | |
| 2077 | if ( TRUE === $zip->open( $archive_path, ZipArchive::CREATE ) ) { |
| 2078 | $zip->addFile( $index_path, 'index.html' ); |
| 2079 | // TODO - add things referenced in wp-content/uploads |
| 2080 | $zip->close(); |
| 2081 | } else { |
| 2082 | error_log( "unable to open zip for creation" ); |
| 2083 | // TODO handle error here |
| 2084 | } |
| 2085 | |
| 2086 | // And remove the HTML file |
| 2087 | unlink( $index_path ); |
| 2088 | |
| 2089 | return( |
| 2090 | array( |
| 2091 | 'timestamp' => $timestamp, |
| 2092 | 'filename' => $archive_filename, |
| 2093 | ) |
| 2094 | ); |
| 2095 | } |
| 2096 | |
| 2097 | // TODO phpDocs |
| 2098 | function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page ) { |
| 2099 | |
| 2100 | // Housekeeping |
| 2101 | $upload_dir = wp_upload_dir(); |
| 2102 | $exports_dir = $upload_dir['basedir'] . '/exports'; |
| 2103 | $exports_url = $upload_dir['baseurl'] . '/exports'; |
| 2104 | |
| 2105 | // Create the exports folder if needed |
| 2106 | $result = wp_mkdir_p( $exports_dir ); |
| 2107 | if ( is_wp_error( $result ) ) { |
| 2108 | return $result; |
| 2109 | } |
| 2110 | |
| 2111 | // TODO Make sure the exports folder is protected (htaccess, index) |
| 2112 | |
| 2113 | // Generate a export file option key from the email address |
| 2114 | $export_file_option = '_wp_privacy_export_file_' . md5( $email_address ); |
| 2115 | |
| 2116 | // See if we have an export URL cached for this user already |
| 2117 | $export_file_details = get_site_option( $export_file_option, '', false ); |
| 2118 | |
| 2119 | // And if we do, short circuit the export and send the path back instead |
| 2120 | if ( ! empty( $export_file_details ) ) { |
| 2121 | $pieces = explode( ':', $export_file_details ); // path:timestamp |
| 2122 | $export_file_path = trailingslashit( $exports_dir ) . $pieces[0]; |
| 2123 | |
| 2124 | if ( file_exists( $export_file_path ) ) { |
| 2125 | return( |
| 2126 | array( |
| 2127 | 'data' => array(), |
| 2128 | 'done' => true, |
| 2129 | 'url' => $pieces[0], |
| 2130 | ) |
| 2131 | ); |
| 2132 | } |
| 2133 | |
| 2134 | // No file? Delete the option and continue building the export |
| 2135 | delete_site_option( $export_file_option ); |
| 2136 | } |
| 2137 | |
| 2138 | // Generate a (temporary) data storage option key from the email address |
| 2139 | $export_data_option = '_wp_privacy_export_data_' . md5( $email_address ); |
| 2140 | |
| 2141 | // First page of first exporter? Prepare/clear the option where we will assemble all the responses |
| 2142 | if ( 1 === $exporter_index && 1 === $page ) { |
| 2143 | update_site_option( $export_data_option, array() ); |
| 2144 | } |
| 2145 | |
| 2146 | // Grab whatever data has already been collected on previous exporters and/or pages |
| 2147 | $export_data = get_site_option( $export_data_option, array() ); |
| 2148 | |
| 2149 | // Does the response include data? If so, add this response's data to all the data we have received so far |
| 2150 | // TODO - enforce the shape? |
| 2151 | // TODO - enforce data as a numeric (not associative) array |
| 2152 | $exporter_page_data = $response['data']; |
| 2153 | if ( ! empty( $exporter_page_data ) && is_array( $exporter_page_data ) ) { |
| 2154 | $export_data = array_merge( $export_data, $exporter_page_data ); |
| 2155 | update_site_option( $export_data_option, $export_data ); |
| 2156 | } |
| 2157 | |
| 2158 | // Are we on the last exporter, and did that export say it was done? If not, return now |
| 2159 | // so we can continue to collect data for export |
| 2160 | $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() ); |
| 2161 | $is_last_exporter = $exporter_index === count( $exporters ); |
| 2162 | $exporter_done = $response['done']; |
| 2163 | if ( ! $is_last_exporter || ! $exporter_done ) { |
| 2164 | return $response; |
| 2165 | } |
| 2166 | |
| 2167 | // TODO de-repetitive-ize the data |
| 2168 | |
| 2169 | // Generate the export |
| 2170 | $result = wp_privacy_generate_personal_data_export( $exports_dir, $email_address, $export_data ); |
| 2171 | if ( is_wp_error( $result ) ) { |
| 2172 | return $result; |
| 2173 | } |
| 2174 | |
| 2175 | // Get the timestamp and filename from the export |
| 2176 | $timestamp = $result['timestamp']; |
| 2177 | $filename = $result['filename']; |
| 2178 | |
| 2179 | // Build the URL for the file |
| 2180 | $export_url = trailingslashit( $exports_url ) . $filename; |
| 2181 | |
| 2182 | // Modify the response to include the URL of the export file |
| 2183 | $response['url'] = $export_url; |
| 2184 | |
| 2185 | // TODO Save the export file in an option for safekeeping |
| 2186 | |
| 2187 | return $response; |
| 2188 | } |
| 2189 | |
| 2190 | function wp_privacy_delete_old_export_files() { |
| 2191 | |
| 2192 | // TODO delete old export files and their options |
| 2193 | |
| 2194 | } |