diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index cbae1c5d68..2e07b4e8fc 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -2990,7 +2990,8 @@ img { /* Metabox collapse arrow indicators */ .sidebar-name .toggle-indicator:before, .js .meta-box-sortables .postbox .toggle-indicator:before, -.bulk-action-notice .toggle-indicator:before { +.bulk-action-notice .toggle-indicator:before, +.privacy-text-box .toggle-indicator:before { content: "\f142"; display: inline-block; font: normal 20px/1 dashicons; @@ -3002,7 +3003,8 @@ img { .js .widgets-holder-wrap.closed .toggle-indicator:before, .js .meta-box-sortables .postbox.closed .handlediv .toggle-indicator:before, -.bulk-action-notice .bulk-action-errors-collapsed .toggle-indicator:before { +.bulk-action-notice .bulk-action-errors-collapsed .toggle-indicator:before, +.privacy-text-box.closed .toggle-indicator:before { content: "\f140"; } diff --git a/src/wp-admin/css/edit.css b/src/wp-admin/css/edit.css index 9c7c257f53..f237d02eb1 100644 --- a/src/wp-admin/css/edit.css +++ b/src/wp-admin/css/edit.css @@ -646,6 +646,68 @@ span.wp-media-buttons-icon:before { margin-bottom: 20px; } +/* Sugested text for privacy policy postbox */ +.privacy-text-box { + margin: 10px 0 0; + +} + +.privacy-text-box-head { + border-left: 4px solid #ffb900; + border-bottom: 1px solid #e5e5e5; + box-shadow: 0 1px 1px rgba(0,0,0,0.04); + +} + +.privacy-text-box .privacy-text-box-head.hndle { + cursor: pointer; +} + +.privacy-text-box-head p { + padding: 0 12px 14px; + margin: 0; +} + +.privacy-text-box-body { + height: 320px; + overflow: auto; + +} + +#privacy-text-box .inside { + margin: 0; +} + +.privacy-text-box h3 { + font-size: 1em; + margin: 1em 0; +} + +.privacy-text-section .privacy-text-copy-button { + float: right; + margin-top: -1.8em; +} + +.privacy-text-section { + padding: 1px 14px 1px 12px; + border-top: 1px solid #e3e3e3; + border-left: 4px solid transparent; +} + +.privacy-text-section.text-updated { + border-left-color: #46b450; + background-color: #ecf7ed; +} + +.privacy-text-section.text-removed { + border-left-color: #dc3232; + background-color: #fbeaea; +} + +.closed .privacy-text-box-body { + display: none; +} + /*------------------------------------------------------------------------------ 11.1 - Custom Fields ------------------------------------------------------------------------------*/ diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php index b9435862ce..b6ee7caf3e 100644 --- a/src/wp-admin/includes/admin-filters.php +++ b/src/wp-admin/includes/admin-filters.php @@ -132,3 +132,16 @@ add_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async add_action( 'upgrader_process_complete', 'wp_version_check', 10, 0 ); add_action( 'upgrader_process_complete', 'wp_update_plugins', 10, 0 ); add_action( 'upgrader_process_complete', 'wp_update_themes', 10, 0 ); + +// Privacy policy text changes check. +add_action( 'admin_init', array( 'WP_Privacy_Policy_Content', 'text_change_check' ), 20 ); + +// Show a "postbox" with the text suggestions for a privacy policy. +add_action( 'edit_form_after_title', array( 'WP_Privacy_Policy_Content', 'privacy_policy_postbox' ) ); + +// Add the suggested policy text from WordPress. +add_action( 'admin_init', array( 'WP_Privacy_Policy_Content', 'add_suggested_content' ), 15 ); + +// Stop checking for text changes after the policy page is updated. +add_action( 'post_updated', array( 'WP_Privacy_Policy_Content', '_policy_page_updated' ) ); + diff --git a/src/wp-admin/includes/misc.php b/src/wp-admin/includes/misc.php index b36fdbfb91..fca15e5be0 100644 --- a/src/wp-admin/includes/misc.php +++ b/src/wp-admin/includes/misc.php @@ -1262,3 +1262,377 @@ All at ###SITENAME### restore_previous_locale(); } } + +/** + * WP_Privacy_Policy_Content class. + * TODO: move this to a new file. + * + * @since 4.9.6 + */ +final class WP_Privacy_Policy_Content { + + private static $policy_content = array(); + + /** + * Constructor + * + * @since 4.9.6 + */ + private function __construct() {} + + /** + * Add privacy information to the postbox shown when editing the privacy policy. + * + * Intended for use from `wp_add_privacy_policy_content()`. + * + * $since 5.0.0 + * + * @param string $plugin_name The plugin'as name. Will be shown in the privacy policy metabox. + * @param string $policy_text The content that should appear in the site's privacy policy. + */ + public static function add( $plugin_name, $policy_text ) { + if ( empty( $plugin_name ) || empty( $policy_text ) ) { + return; + } + + $data = array( + 'plugin_name' => $plugin_name, + 'policy_text' => $policy_text, + ); + + if ( ! in_array( $data, self::$policy_content, true ) ) { + self::$policy_content[] = $data; + } + } + + /** + * Quick check if any privacy info has changed. + * + * @since 4.9.6 + */ + public static function text_change_check() { + + $policy_page_id = (int) get_option( 'wp_page_for_privacy_policy' ); + + // The site doesn't have a privacy policy. + if ( empty( $policy_page_id ) ) { + return; + } + + if ( ! current_user_can( 'edit_post', $policy_page_id ) ) { + return; + } + + // Also run when the option doesn't exist yet. + if ( get_option( '_wp_privacy_text_change_check' ) === 'no-check' ) { + return; + } + + $old = (array) get_post_meta( $policy_page_id, '_wp_suggested_privacy_policy_content' ); + $new = self::$policy_content; + + // Remove the extra values added to the meta. + foreach ( $old as $key => $data ) { + $old[ $key ] = array( + 'plugin_name' => $data['plugin_name'], + 'policy_text' => $data['policy_text'], + ); + } + + // The == operator (equal, not identical) was used intentionally. + // See http://php.net/manual/en/language.operators.array.php + if ( $new != $old ) { + // A plugin was activated or deactivated, or some policy text has changed. + // Show a notice on all screens in wp-admin. + add_action( 'admin_notices', array( 'WP_Privacy_Policy_Content', 'policy_text_changed_notice' ) ); + } else { + // Stop checking. + update_option( '_wp_privacy_text_change_check', 'no-check' ); + } + } + + /** + * Output an admin notice when some privacy info has changed. + * + * @since 4.9.6 + */ + public static function policy_text_changed_notice() { + global $post; + $policy_page_id = (int) get_option( 'wp_page_for_privacy_policy' ); + + ?> +
+

ID != $policy_page_id ) { + ?> + +

+
+ $old_data ) { + if ( ! empty( $old_data['removed'] ) ) { + // Remove the old policy text. + $update_cache = true; + continue; + } + + if ( ! empty( $old_data['updated'] ) ) { + // 'updated' is now 'added'. + $done[] = array( + 'plugin_name' => $old_data['plugin_name'], + 'policy_text' => $old_data['policy_text'], + 'added' => $old_data['updated'], + ); + $update_cache = true; + } else { + $done[] = $old_data; + } + } + + if ( $update_cache ) { + delete_post_meta( $policy_page_id, '_wp_suggested_privacy_policy_content' ); + // Update the cache. + foreach ( $done as $data ) { + add_post_meta( $policy_page_id, '_wp_suggested_privacy_policy_content', $data ); + } + } + } + + /** + * Check for updated, added or removed privacy policy information from plugins. + * + * Caches the current info in post_meta of the policy page. + * + * @since 4.9.6 + * + * @return array The privacy policy text/informtion added by core and plugins. + */ + public static function get_suggested_policy_text() { + $policy_page_id = (int) get_option( 'wp_page_for_privacy_policy' ); + $new = self::$policy_content; + $old = (array) get_post_meta( $policy_page_id, '_wp_suggested_privacy_policy_content' ); + $checked = array(); + $time = time(); + $update_cache = false; + + // Check for no-changes and updates. + foreach ( $new as $new_key => $new_data ) { + foreach ( $old as $old_key => $old_data ) { + $found = false; + + if ( $new_data['policy_text'] === $old_data['policy_text'] ) { + // Use the new plugin name in case it was changed, translated, etc. + if ( $old_data['plugin_name'] !== $new_data['plugin_name'] ) { + $old_data['plugin_name'] = $new_data['plugin_name']; + $update_cache = true; + } + + // A plugin was re-activated. + if ( ! empty( $old_data['removed'] ) ) { + unset( $old_data['removed'] ); + $old_data['added'] = $time; + $update_cache = true; + } + + $checked[] = $old_data; + $found = true; + } elseif ( $new_data['plugin_name'] === $old_data['plugin_name'] ) { + // The info for the policy was updated. + $checked[] = array( + 'plugin_name' => $new_data['plugin_name'], + 'policy_text' => $new_data['policy_text'], + 'updated' => $time, + ); + $found = $update_cache = true; + } + + if ( $found ) { + unset( $new[ $new_key ], $old[ $old_key ] ); + continue 2; + } + } + } + + if ( ! empty( $new ) ) { + // A plugin was activated. + foreach ( $new as $new_data ) { + if ( ! empty( $new_data['plugin_name'] ) && ! empty( $new_data['policy_text'] ) ) { + $new_data['added'] = $time; + array_unshift( $checked, $new_data ); + } + } + $update_cache = true; + } + + if ( ! empty( $old ) ) { + // A plugin was deactivated. + foreach ( $old as $old_data ) { + if ( ! empty( $old_data['plugin_name'] ) && ! empty( $old_data['policy_text'] ) ) { + $data = array( + 'plugin_name' => $old_data['plugin_name'], + 'policy_text' => $old_data['policy_text'], + 'removed' => $time, + ); + array_unshift( $checked, $data ); + } + } + $update_cache = true; + } + + if ( $update_cache ) { + delete_post_meta( $policy_page_id, '_wp_suggested_privacy_policy_content' ); + // Update the cache. + foreach ( $checked as $data ) { + add_post_meta( $policy_page_id, '_wp_suggested_privacy_policy_content', $data ); + } + } + + // Stop checking for changes after the postbox has been loaded. + // TODO make this user removable? + if ( get_option( '_wp_privacy_text_change_check' ) !== 'no-check' ) { + update_option( '_wp_privacy_text_change_check', 'no-check' ); + } + + return $checked; + } + + /** + * Output the postbox when editing the privacy policy page + * + * @since 4.9.6 + * + * @param $post WP_Post The currently edited post. + */ + public static function privacy_policy_postbox( $post ) { + if ( ! ( $post instanceof WP_Post ) ) { + return; + } + + $policy_page_id = (int) get_option( 'wp_page_for_privacy_policy' ); + + if ( ! $policy_page_id || $policy_page_id != $post->ID ) { + return; + } + + $content_array = self::get_suggested_policy_text(); + + $content = ''; + $date_format = get_option( 'date_format' ); + $copy = __( 'Copy' ); + + foreach ( $content_array as $section ) { + $class = $meta = ''; + + if ( ! empty( $section['removed'] ) ) { + $class = ' text-removed'; + $date = date_i18n( $date_format, $section['removed'] ); + $meta = sprintf( __( 'Policy text removed %s.' ), $date ); + } elseif ( ! empty( $section['updated'] ) ) { + $class = ' text-updated'; + $date = date_i18n( $date_format, $section['updated'] ); + $meta = sprintf( __( 'Policy text last updated %s.' ), $date ); + } elseif ( ! empty( $section['added'] ) ) { + $class = ' text-added'; + $date = date_i18n( $date_format, $section['added'] ); + $meta = sprintf( __( 'Policy text added %s.' ), $date ); + } + + $content .= '
'; + $content .= '

' . $section['plugin_name'] . '

'; + $content .= ''; + + if ( ! empty( $meta ) ) { + $content .= '' . $meta . ''; + } + + $content .= '
' . $section['policy_text'] . '
'; + $content .= "
\n"; + } + + ?> +
+ +
+

+

+ + +

+
+ +
+ +
+
+ ' . __( 'Lorem ipsum dolor sit amet consectetuer id elit enim neque est. Sodales tincidunt Nulla leo penatibus Vestibulum adipiscing est cursus Nam Vestibulum. Orci Vivamus mollis eget pretium dictumst Donec Integer auctor sociis rutrum. Mauris felis Donec neque cursus tellus odio adipiscing netus elit Donec. Vestibulum Cras ligula vitae pretium Curabitur eros Nam Lorem eros non. Sed id mauris justo tristique orci neque eleifend lacus lorem.' ) . "

\n"; + $content .= '

' . __( 'Sed consequat Nullam et vel platea semper id mauris Nam eget. Sem neque a amet eu ipsum id dignissim neque eu pulvinar. Mauris nulla egestas et laoreet penatibus ipsum lobortis convallis congue libero. Tortor nibh pellentesque tellus odio Morbi cursus eros tincidunt tincidunt sociis. Egestas at In Donec mi dignissim Nam rutrum felis metus Maecenas. Sed tellus consectetuer.' ) . "

\n"; + $content .= '

' . __( 'Justo orci pulvinar mauris tincidunt sed Pellentesque dis sapien tempor ligula. Dolor laoreet fames eros accumsan Integer feugiat nec augue Phasellus rutrum. Id Sed facilisi elit mus nulla at dapibus ut enim sociis. Fringilla ridiculus dui justo eu Maecenas ipsum ut aliquet magna non. Id magna adipiscing Vestibulum Curabitur vel pretium ac justo platea neque. Maecenas Donec Quisque urna interdum.' ) . "

\n"; + $content .= '

' . __( 'Tellus sagittis leo adipiscing ante facilisis Aliquam tellus at at elit. Ut dignissim tempus eu Fusce Vestibulum at eros ante dis tempus. Sed libero orci at id ut pretium metus adipiscing justo malesuada. In tempus vitae commodo libero In neque sagittis turpis In In. Eleifend elit dis ac eros urna auctor semper quis odio pretium. Ut Aenean cursus.' ) . "

\n"; + + /** + * Filters the default content suggested for inclusion in a privacy policy. + * + * @since 4.9.6 + * + * @param $content string The defauld policy content. + */ + return apply_filters( 'wp_get_default_privcy_policy_content', $content ); + } + + /** + * Add the suggested privacy policy text to the policy postbox. + * + * @since 4.9.6 + */ + public static function add_suggested_content() { + $content = self::get_default_content(); + wp_add_privacy_policy_content( 'WordPress', $content ); + } +} diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index f2758ae1cd..cbf617fd96 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2014,3 +2014,22 @@ function plugin_sandbox_scrape( $plugin ) { wp_register_plugin_realpath( WP_PLUGIN_DIR . '/' . $plugin ); include( WP_PLUGIN_DIR . '/' . $plugin ); } + +/** + * Helper function for adding plugin specific information to the postbox shown when editing the privacy policy. + * + * Intended for use with the `'admin_init'` action. + * + * @since 4.9.6 + * + * @param string $plugin_name The plugin'as name. Will be shown in the privacy policy metabox. + * @param string $policy_text The content that should appear in the site's privacy policy. + * For more information see the Plugins Handbook https://developer.wordpress.org/plugins/. + */ +function wp_add_privacy_policy_content( $plugin_name, $policy_text ) { + if ( ! class_exists( 'WP_Privacy_Policy_Content' ) ) { + require_once( ABSPATH . 'wp-admin/includes/misc.php' ); + } + + WP_Privacy_Policy_Content::add( $plugin_name, $policy_text ); +} diff --git a/src/wp-admin/js/post.js b/src/wp-admin/js/post.js index 553d3999f3..2f1f9509bb 100644 --- a/src/wp-admin/js/post.js +++ b/src/wp-admin/js/post.js @@ -1267,4 +1267,27 @@ jQuery(document).ready( function($) { update(); } ); + + // Privacy policy postbox, copy button. + $( document ).on( 'click', function( event ) { + var $target = $( event.target ); + var node, range; + + if ( $target.is( 'button.privacy-text-copy-button' ) ) { + node = $target.parent().find( 'div.policy-text' )[0]; + + if ( node ) { + try { + window.getSelection().removeAllRanges(); + range = document.createRange(); + range.selectNode( node ); + window.getSelection().addRange( range ); + + document.execCommand( 'copy' ); + window.getSelection().removeAllRanges(); + } catch ( er ) {} + } + } + }); + } )( jQuery, new wp.utils.WordCounter() ); diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 96d5fc3fa4..bd8500e28d 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -552,4 +552,8 @@ add_filter( 'pre_oembed_result', 'wp_filter_pre_oembed_result', 10, 3 ); // Capabilities add_filter( 'user_has_cap', 'wp_maybe_grant_install_languages_cap', 1 ); +// Trigger the check for policy text changes after active plugins change. +add_action( 'update_site_option_active_sitewide_plugins', '_wp_privacy_active_plugins_change' ); +add_action( 'update_option_active_plugins', '_wp_privacy_active_plugins_change' ); + unset( $filter, $action ); diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index d53939caeb..6f9e77c5b7 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -6245,3 +6245,13 @@ function wp_privacy_anonymize_data( $type, $data = '' ) { */ return apply_filters( 'wp_privacy_anonymize_data', $anonymous, $type, $data ); } + +/** + * Trigger the check for policy text changes. + * + * @since 4.9.6 + * @access private + */ +function _wp_privacy_active_plugins_change() { + update_option( '_wp_privacy_text_change_check', 'check' ); +} diff --git a/src/wp-includes/link-template.php b/src/wp-includes/link-template.php index 365b14b317..e1d12f089f 100644 --- a/src/wp-includes/link-template.php +++ b/src/wp-includes/link-template.php @@ -4273,3 +4273,20 @@ function get_parent_theme_file_path( $file = '' ) { */ return apply_filters( 'parent_theme_file_path', $path, $file ); } + +/** + * Retrieves the URL to the privacy policy page. + * + * @since 4.9.6 + * + * @return string The URL to the privacy policy page. Empty string if it doesn't exist. + */ +function get_privacy_policy_url() { + $policy_page_id = (int) get_option( 'wp_page_for_privacy_policy' ); + + if ( empty( $policy_page_id ) ) { + return ''; + } + + return get_permalink( $policy_page_id ); +}