<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[43046] trunk/src: Privacy: Add cron to delete expired export files to protect privacy.</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/43046">43046</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/43046","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>iandunn</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2018-04-30 20:08:37 +0000 (Mon, 30 Apr 2018)</dd>
</dl>
<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Privacy: Add cron to delete expired export files to protect privacy.
The primary means of protecting the files is the CSPRN appended to the filename, but there is no reason to keep the files after the data subject has downloaded them, so deleting them provides an additional layer of protection. Previously this was done from `wp_privacy_generate_personal_data_export_file()`, but that does not guarantee that it will be run regularly, and on smaller sites that could result in export files being exposed for much longer than necessary.
`wp_privacy_delete_old_export_files()` was moved to a front end file, so that it can be called from `cron.php`.
This introduces the `wp_privacy_export_expiration` filter, which allows plugins to customize how long the exports are kept before being deleted.
`index.html` was added to the `$exclusions` parameter of `list_files()` to make sure that it isn't deleted. If it were, then poorly-configured servers would allow the directory to be traversed, exposing all of the exported files.
Props iandunn, desrosj.
See <a href="https://core.trac.wordpress.org/ticket/43546">#43546</a>.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpadminincludesfilephp">trunk/src/wp-admin/includes/file.php</a></li>
<li><a href="#trunksrcwpincludesdefaultfiltersphp">trunk/src/wp-includes/default-filters.php</a></li>
<li><a href="#trunksrcwpincludesfunctionsphp">trunk/src/wp-includes/functions.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpadminincludesfilephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/file.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/file.php 2018-04-30 18:52:59 UTC (rev 43045)
+++ trunk/src/wp-admin/includes/file.php 2018-04-30 20:08:37 UTC (rev 43046)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1999,9 +1999,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * @param int $request_id The export request ID.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> function wp_privacy_generate_personal_data_export_file( $request_id ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- // Maybe make this a cron job instead.
- wp_privacy_delete_old_export_files();
-
</del><span class="cx" style="display: block; padding: 0 10px"> if ( ! class_exists( 'ZipArchive' ) ) {
</span><span class="cx" style="display: block; padding: 0 10px"> wp_send_json_error( __( 'Unable to generate export file. ZipArchive not available.' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2162,13 +2159,18 @@
</span><span class="cx" style="display: block; padding: 0 10px"> return new WP_Error( 'invalid', __( 'Invalid request ID when sending personal data export email.' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-/* translators: Do not translate LINK, EMAIL, SITENAME, SITEURL: those are placeholders. */
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /** This filter is documented in wp-admin/includes/file.php */
+ $expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS );
+ $expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration );
+
+/* translators: Do not translate EXPIRATION, LINK, EMAIL, SITENAME, SITEURL: those are placeholders. */
</ins><span class="cx" style="display: block; padding: 0 10px"> $email_text = __(
</span><span class="cx" style="display: block; padding: 0 10px"> 'Howdy,
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> Your request for an export of personal data has been completed. You may
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-download your personal data by clicking on the link below. This link is
-good for the next 3 days.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+download your personal data by clicking on the link below. For privacy
+and security, we will automatically delete the file on ###EXPIRATION###,
+so please download it before then.
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> ###LINK###
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2183,6 +2185,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * Filters the text of the email sent with a personal data export file.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * The following strings have a special meaning and will get replaced dynamically:
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * ###EXPIRATION### The date when the URL will be automatically deleted.
</ins><span class="cx" style="display: block; padding: 0 10px"> * ###LINK### URL of the personal data export file for the user.
</span><span class="cx" style="display: block; padding: 0 10px"> * ###EMAIL### The email we are sending to.
</span><span class="cx" style="display: block; padding: 0 10px"> * ###SITENAME### The name of the site.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2200,6 +2203,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $site_name = is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' );
</span><span class="cx" style="display: block; padding: 0 10px"> $site_url = network_home_url();
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $content = str_replace( '###EXPIRATION###', $expiration_date, $content );
</ins><span class="cx" style="display: block; padding: 0 10px"> $content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content );
</span><span class="cx" style="display: block; padding: 0 10px"> $content = str_replace( '###EMAIL###', $email_address, $content );
</span><span class="cx" style="display: block; padding: 0 10px"> $content = str_replace( '###SITENAME###', wp_specialchars_decode( $site_name, ENT_QUOTES ), $content );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2344,22 +2348,3 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> return $response;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-/**
- * Cleans up export files older than three days old.
- *
- * @since 4.9.6
- */
-function wp_privacy_delete_old_export_files() {
- $upload_dir = wp_upload_dir();
- $exports_dir = trailingslashit( $upload_dir['basedir'] . '/exports' );
- $export_files = list_files( $exports_dir );
-
- foreach( (array) $export_files as $export_file ) {
- $file_age_in_seconds = time() - filemtime( $export_file );
-
- if ( 3 * DAY_IN_SECONDS < $file_age_in_seconds ) {
- @unlink( $export_file );
- }
- }
-}
</del></span></pre></div>
<a id="trunksrcwpincludesdefaultfiltersphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/default-filters.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/default-filters.php 2018-04-30 18:52:59 UTC (rev 43045)
+++ trunk/src/wp-includes/default-filters.php 2018-04-30 20:08:37 UTC (rev 43046)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -352,6 +352,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'user_request_action_confirmed_message', '_wp_privacy_account_request_confirmed_message', 10, 2 );
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'wp_privacy_personal_data_exporters', 'wp_register_comment_personal_data_exporter' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'wp_privacy_personal_data_erasers', 'wp_register_comment_personal_data_eraser' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+add_action( 'init', 'wp_schedule_delete_old_privacy_export_files' );
+add_action( 'wp_privacy_delete_old_export_files', 'wp_privacy_delete_old_export_files' );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> // Cron tasks
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'wp_scheduled_delete', 'wp_scheduled_delete' );
</span></span></pre></div>
<a id="trunksrcwpincludesfunctionsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/functions.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/functions.php 2018-04-30 18:52:59 UTC (rev 43045)
+++ trunk/src/wp-includes/functions.php 2018-04-30 20:08:37 UTC (rev 43046)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -6257,3 +6257,51 @@
</span><span class="cx" style="display: block; padding: 0 10px"> function _wp_privacy_active_plugins_change() {
</span><span class="cx" style="display: block; padding: 0 10px"> update_option( '_wp_privacy_text_change_check', 'check' );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+/**
+ * Schedule a `WP_Cron` job to delete expired export files.
+ *
+ * @since 4.9.6
+ */
+function wp_schedule_delete_old_privacy_export_files() {
+ if ( ! wp_next_scheduled( 'wp_privacy_delete_old_export_files' ) ) {
+ wp_schedule_event( time(), 'hourly', 'wp_privacy_delete_old_export_files' );
+ }
+}
+
+/**
+ * Cleans up export files older than three days old.
+ *
+ * The export files are stored in `wp-content/uploads`, and are therefore publicly
+ * accessible. A CSPRN is appended to the filename to mitigate the risk of an
+ * unauthorized person downloading the file, but it is still possible. Deleting
+ * the file after the data subject has had a chance to delete it adds an additional
+ * layer of protection.
+ *
+ * @since 4.9.6
+ */
+function wp_privacy_delete_old_export_files() {
+ $upload_dir = wp_upload_dir();
+ $exports_dir = trailingslashit( $upload_dir['basedir'] . '/exports' );
+ $export_files = list_files( $exports_dir, 100, array( 'index.html' ) );
+
+ /**
+ * Filters the lifetime, in seconds, of a personal data export file.
+ *
+ * By default, the lifetime is 3 days. Once the file reaches that age, it will automatically
+ * be deleted by a cron job.
+ *
+ * @since 4.9.6
+ *
+ * @param int $expiration The expiration age of the export, in seconds.
+ */
+ $expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS );
+
+ foreach ( (array) $export_files as $export_file ) {
+ $file_age_in_seconds = time() - filemtime( $export_file );
+
+ if ( $expiration < $file_age_in_seconds ) {
+ unlink( $export_file );
+ }
+ }
+}
</ins></span></pre>
</div>
</div>
</body>
</html>