diff --git a/_inc/client/security/index.jsx b/_inc/client/security/index.jsx index 15a149da7e32d..f9bb266f1a166 100644 --- a/_inc/client/security/index.jsx +++ b/_inc/client/security/index.jsx @@ -21,6 +21,7 @@ import BackupsScan from './backups-scan'; import Antispam from './antispam'; import { ManagePlugins } from './manage-plugins'; import { Monitor } from './monitor'; +import { Private } from './private'; import { Protect } from './protect'; import { SSO } from './sso'; @@ -72,13 +73,14 @@ export class Security extends Component { foundAkismet = this.isAkismetFound(), rewindActive = 'active' === get( this.props.rewindStatus, [ 'state' ], false ), foundBackups = this.props.isModuleFound( 'vaultpress' ) || rewindActive, - foundMonitor = this.props.isModuleFound( 'monitor' ); + foundMonitor = this.props.isModuleFound( 'monitor' ), + foundPrivateSites = this.props.isModuleFound( 'private' ); if ( ! this.props.searchTerm && ! this.props.active ) { return null; } - if ( ! foundSso && ! foundProtect && ! foundAkismet && ! foundBackups && ! foundMonitor ) { + if ( ! foundSso && ! foundProtect && ! foundAkismet && ! foundBackups && ! foundMonitor && ! foundPrivateSites ) { return null; } @@ -106,6 +108,7 @@ export class Security extends Component { { foundProtect && } { foundSso && } + { foundPrivateSites && } ); } diff --git a/_inc/client/security/private.jsx b/_inc/client/security/private.jsx new file mode 100644 index 0000000000000..fd671cda28008 --- /dev/null +++ b/_inc/client/security/private.jsx @@ -0,0 +1,52 @@ +/** + * External dependencies + */ +import React, { Component } from 'react'; +import { translate as __ } from 'i18n-calypso'; + +/** + * Internal dependencies + */ +import { ModuleToggle } from 'components/module-toggle'; +import SettingsCard from 'components/settings-card'; +import SettingsGroup from 'components/settings-group'; +import { withModuleSettingsFormHelpers } from 'components/module-settings/with-module-settings-form-helpers'; + +export const Private = withModuleSettingsFormHelpers( + class extends Component { + render() { + return ( + + +

+ { __( + 'Private sites can only be seen by you ' + + 'and other users who are members of this site.' + ) } +

+ + + { __( 'Make your site private' ) } + +
+
+ ); + } + } +); diff --git a/bin/phpcs-whitelist.js b/bin/phpcs-whitelist.js index 90066fb4be093..ee0a3d103e776 100644 --- a/bin/phpcs-whitelist.js +++ b/bin/phpcs-whitelist.js @@ -22,4 +22,6 @@ module.exports = [ 'modules/verification-tools.php', 'modules/wpcom-block-editor/class-jetpack-wpcom-block-editor.php', 'packages', + 'modules/private.php', + 'modules/private/', ]; diff --git a/class.jetpack.php b/class.jetpack.php index e7e6a0d451711..7a4ff8560a18c 100644 --- a/class.jetpack.php +++ b/class.jetpack.php @@ -1809,9 +1809,80 @@ function user_role_change( $user_id ) { } /** - * Loads the currently active modules. + * Loads the private module if it has been activated. + * Else, updates the admin dashboard with the site's private status. */ - public static function load_modules() { + public static function load_private() { + if ( self::is_module_active( 'private' ) ) { + self::load_modules( array( 'private' ) ); + } else { + add_action( 'update_right_now_text', array( __CLASS__, 'add_public_dashboard_glance_items' ) ); + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'wp_admin_glance_dashboard_style' ) ); + add_filter( 'privacy_on_link_text', array( __CLASS__, 'private_site_privacy_on_link_text' ) ); + } + } + + /** + * Adds a line break for the 'Search Engines Discouraged' message + * displayed in the 'At a Glance' dashboard widget. + * + * @param string $content Content of 'At A Glance' wp-admin dashboard widget. + * @return string The modified content of the 'At a Glance' dashboard widget. + */ + + public static function private_site_privacy_on_link_text( $content ) { + return '
' . $content; + } + + /** + * Basic styling for the wp-admin 'At a Glance' dashboard widget. + * This is applied when the private module is inactive. + * + * @param string $hook Page Hook Suffix for the current page. + */ + public static function wp_admin_glance_dashboard_style( $hook ) { + if ( 'index.php' !== $hook ) { + return; + } + + $custom_css = ' + .jp-at-a-glance__site-public { + color: #46B450; + } + '; + wp_add_inline_style( 'dashboard', $custom_css ); + } + + /** + * Adds a message to the 'At a Glance' dashboard widget. + * + * @param string $content Content of 'At A Glance' wp-admin dashboard widget. + * @return string The modified content of the 'At a Glance' dashboard widget. + */ + public static function add_public_dashboard_glance_items( $content ) { + return + $content . + '

' . + wp_kses( + sprintf( + /* translators: URL for Jetpack dashboard. */ + __( 'This site is set to public. Make private.', 'jetpack' ), + esc_attr( 'jp-at-a-glance__site-public' ), + esc_url( admin_url( 'admin.php?page=jetpack#/security?term=private' ) ) + ), + array( + 'a' => array( 'href' => true ), + 'span' => array( 'class' => true ), + ) + ); + } + + /** + * Loads modules from given array, otherwise all the currently active modules. + * + * @param array $modules Specific modules to be loaded. + */ + public static function load_modules( $modules = array() ) { if ( ! self::is_active() && ! self::is_development_mode() @@ -1831,9 +1902,13 @@ public static function load_modules() { do_action( 'updating_jetpack_version', $version, false ); Jetpack_Options::update_options( compact( 'version', 'old_version' ) ); } - list( $version ) = explode( ':', $version ); + list( $version ) = explode( ':', $version ); + $fetched_all_active_modules = false; - $modules = array_filter( Jetpack::get_active_modules(), array( 'Jetpack', 'is_module' ) ); + if ( empty( $modules ) ) { + $modules = array_filter( Jetpack::get_active_modules(), array( 'Jetpack', 'is_module' ) ); + $fetched_all_active_modules = true; + } $modules_data = array(); @@ -1890,12 +1965,14 @@ public static function load_modules() { do_action( 'jetpack_module_loaded_' . $module ); } - /** - * Fires when all the modules are loaded. - * - * @since 1.1.0 - */ - do_action( 'jetpack_modules_loaded' ); + if ( $fetched_all_active_modules ) { + /** + * Fires when all the modules are loaded. + * + * @since 1.1.0 + */ + do_action( 'jetpack_modules_loaded' ); + } // Load module-specific code that is needed even when a module isn't active. Loaded here because code contained therein may need actions such as setup_theme. require_once( JETPACK__PLUGIN_DIR . 'modules/module-extras.php' ); diff --git a/jetpack.php b/jetpack.php index 695b37db5211c..465d6b2633274 100644 --- a/jetpack.php +++ b/jetpack.php @@ -232,6 +232,7 @@ function jetpack_admin_missing_autoloader() { ?> add_action( 'updating_jetpack_version', array( 'Jetpack', 'do_version_bump' ), 10, 2 ); add_action( 'init', array( 'Jetpack', 'init' ) ); add_action( 'plugins_loaded', array( 'Jetpack', 'plugin_textdomain' ), 99 ); +add_action( 'plugins_loaded', array( 'Jetpack', 'load_private' ), 99 ); add_action( 'plugins_loaded', array( 'Jetpack', 'load_modules' ), 100 ); add_filter( 'jetpack_static_url', array( 'Jetpack', 'staticize_subdomain' ) ); add_filter( 'is_jetpack_site', '__return_true' ); diff --git a/modules/module-extras.php b/modules/module-extras.php index 989512dcbf516..e762d38a047ec 100644 --- a/modules/module-extras.php +++ b/modules/module-extras.php @@ -83,3 +83,28 @@ function jetpack_widgets_add_suffix( $widget_name ) { ); } add_filter( 'jetpack_widget_name', 'jetpack_widgets_add_suffix' ); + +add_action( 'blog_privacy_selector', 'jetpack_priv_notice_privacy_selector' ); + +/** + * Echos notice directing site owners to Jetpack's Private Site feature. + */ +function jetpack_priv_notice_privacy_selector() { + ?> +

+ Go to Private Site settings.', 'jetpack' ), + esc_url( admin_url( 'admin.php?page=jetpack#/security?term=private' ) ) + ), + array( 'a' => array( 'href' => true ) ) + ); + ?> +

+ + _x( 'Publish posts by sending an email', 'Module Description', 'jetpack' ), ), + 'private' => array( + 'name' => _x( 'Private site', 'Module Name', 'jetpack' ), + 'description' => _x( 'Make your site only visible to you and users you approve.', 'Module Description', 'jetpack' ), + ), + 'protect' => array( 'name' => _x( 'Protect', 'Module Name', 'jetpack' ), 'description' => _x( 'Protect yourself from brute force and distributed brute force attacks, which are the most common way for hackers to get into your site.', 'Module Description', 'jetpack' ), @@ -328,6 +333,10 @@ function jetpack_get_module_i18n_tag( $key ) { // - modules/minileven.php 'Mobile' =>_x( 'Mobile', 'Module Tag', 'jetpack' ), + // Modules with `Private` tag: + // - modules/private.php + 'Private' =>_x( 'Private', 'Module Tag', 'jetpack' ), + // Modules with `Traffic` tag: // - modules/sitemaps.php // - modules/wordads.php diff --git a/modules/module-info.php b/modules/module-info.php index f13964de081a0..b3b0b2d7d1a7d 100644 --- a/modules/module-info.php +++ b/modules/module-info.php @@ -912,3 +912,22 @@ function jetpack_more_info_copy_post() { esc_html_e( 'Create a new post based on an existing post.', 'jetpack' ); } add_action( 'jetpack_module_more_info_copy-post', 'jetpack_more_info_copy_post' ); + +/** + * Private sites support link. + */ +function jetpack_private_more_link() { + echo 'https://jetpack.com/support/private'; +} +add_action( 'jetpack_learn_more_button_private', 'jetpack_private_more_link' ); + +/** + * Private sites description. + */ +function jetpack_private_more_info() { + esc_html_e( + 'Make your site private. It will only be visible to registered users.', + 'jetpack' + ); +} +add_action( 'jetpack_module_more_info_private', 'jetpack_private_more_info' ); diff --git a/modules/private.php b/modules/private.php new file mode 100644 index 0000000000000..cb54988da9591 --- /dev/null +++ b/modules/private.php @@ -0,0 +1,19 @@ +query_vars['robots'] ) ) { + return; + } + + if ( is_user_logged_in() && self::is_private_blog_user( get_current_blog_id() ) ) { + return; + } + + include JETPACK__PLUGIN_DIR . '/modules/private/private.php'; + + exit; + } + + /** + * Does not check whether the blog is private. Accepts blog and user in various types. + * Returns true for super admins. + * + * @param int $blog Current WordPress blod id.. + */ + private static function is_private_blog_user( $blog ) { + if ( is_numeric( $blog ) ) { + $blog_id = intval( $blog ); + } elseif ( is_object( $blog ) ) { + $blog_id = $blog->blog_id; + } elseif ( is_string( $blog ) ) { + $fields = array( + 'domain' => $blog, + 'path' => '/', + ); + $blog = get_blog_details( $fields ); + $blog_id = $blog->blog_id; + } else { + $blog_id = get_current_blog_id(); + } + + /** + * Filter the capabilites a user needs to have to see the site + * + * @module private + * + * @since 7.4.0 + * + * @param string $cap The lowest capability a user needs to have + */ + $capability = apply_filters( 'jetpack_private_capability', 'read' ); + return current_user_can_for_blog( $blog_id, $capability ); + } + + /** + * Hides the blog's name on the login form for private blogs. + */ + public static function privatize_blog_maybe_mask_blog_name() { + add_filter( 'bloginfo', array( __CLASS__, 'privatize_blog_mask_blog_name' ), 3, 2 ); + } + + /** + * Replaces the the blog's "name" value with "Private Site" + * + * @see privatize_blog_maybe_mask_blog_name() + * @param mixed $value The requested non-URL site information. + * @param mixed $what Type of information requested. + */ + public static function privatize_blog_mask_blog_name( $value, $what ) { + if ( in_array( $what, array( 'name', 'title' ), true ) ) { + $value = __( 'Private Site', 'jetpack' ); + } + + return $value; + } + + /** + * Remove the privatize_blog_mask_blog_name filter + */ + public static function remove_privatize_blog_mask_blog_name_filter() { + remove_filter( 'bloginfo', array( __CLASS__, 'privatize_blog_mask_blog_name' ) ); + } + + /** + * Filters new comments so that users can't comment on private blogs + * + * @param array $comment Documented in wp-includes/comment.php. + */ + public static function privatize_blog_comments( $comment ) { + self::privatize_blog( null ); + return $comment; + } + + /** + * Extend the 'Site Visibility' privacy options to also include a private option + **/ + public static function privatize_blog_priv_selector() { + ?> + + +
+ made your site private.', 'jetpack' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + esc_url( admin_url( 'admin.php?page=jetpack#/security?term=private' ) ) + ), + array( 'a' => array( 'href' => true ) ) + ); + ?> +
+
+ '; + } + + /** + * Prevents ajax and post requests on private blogs for users who don't have permissions + */ + public static function private_blog_prevent_requests() { + global $pagenow; + + $is_ajax_request = defined( 'DOING_AJAX' ) && DOING_AJAX; + $is_admin_post_request = ( 'admin-post.php' === $pagenow ); + + // Make sure we are in the right code path, if not bail now. + if ( ! is_admin() || ( ! $is_ajax_request && ! $is_admin_post_request ) ) { + return; + } + + if ( ! self::is_private_blog_user( get_current_blog_id() ) ) { + wp_die( esc_html__( 'This site is private.', 'jetpack' ), 403 ); + } + } + + /** + * Prevents ajax requests on private blogs for users who don't have permissions + * + * @param string $action The Ajax nonce action. + * @param false|int $result The result of the nonce check. + */ + public static function private_blog_ajax_nonce_check( $action, $result ) { + if ( 1 !== $result && 2 !== $result ) { + return; + } + + // These two ajax actions relate to wp_ajax_wp_link_ajax() and wp_ajax_find_posts() + // They are needed for users with admin capabilities in wp-admin. + // Read more at p3btAN-o8-p2. + if ( 'find-posts' !== $action && 'internal-linking' !== $action ) { + return; + } + + // Make sure we are in the right code path, if not bail now. + if ( ! is_admin() || ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) ) { + return; + } + + if ( ! self::is_private_blog_user( get_current_blog_id() ) ) { + wp_die( esc_html__( 'This site is private.', 'jetpack' ), 403 ); + } + } + + /** + * Disables WordPress Rest API for external requests + */ + public static function disable_rest_api() { + if ( is_user_logged_in() && self::is_private_blog_user( get_current_blog_id() ) ) { + return; + } + + if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { + return new WP_Error( 'private_site', __( 'This site is private.', 'jetpack' ), array( 'status' => 403 ) ); + } + } + + /** + * Disables modules for private sites + * + * @param array $modules Available modules. + * + * @return array Array of modules after filtering. + */ + public static function private_get_modules( $modules ) { + $disabled_modules = array( + 'publicize', + 'sharedaddy', + 'subscriptions', + 'json-api', + 'enhanced-distribution', + 'google-analytics', + 'photon', + 'sitemaps', + 'verification-tools', + 'wordads', + ); + foreach ( $disabled_modules as $module_slug ) { + $found = array_search( $module_slug, $modules, true ); + if ( false !== $found ) { + unset( $modules[ $found ] ); + } + } + return $modules; + } + + /** + * Show an error when the blog_public option is updated + */ + public static function private_update_option_blog_public() { + if ( function_exists( 'add_settings_error' ) ) { + /* translators: URL for Jetpack dashboard. */ + add_settings_error( + 'general', + 'setting_not_updated', + wp_kses( + sprintf( + /* translators: URL for Jetpack dashboard. */ + __( 'This setting is ignored because you made your site private.', 'jetpack' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + esc_url( admin_url( 'admin.php?page=jetpack#/security?term=private' ) ) + ), + array( 'a' => array( 'href' => true ) ) + ) + ); + } + } + + /** + * Basic styling for the wp-admin 'At a Glance' dashboard widget. + * + * @param string $hook Page Hook Suffix for the current page. + */ + public static function wp_admin_glance_dashboard_style( $hook ) { + if ( 'index.php' !== $hook ) { + return; + } + $custom_css = ' + .jp-at-a-glance__site-private { + color: #DC3232; + } + '; + wp_add_inline_style( 'dashboard', $custom_css ); + } + + /** + * Adds a message to the 'At a Glance' dashboard widget. + * + * @param string $content Content of At A Glance wp-admin dashboard widget. + * @return string The modified content of the 'At a Glance' dashboard widget. + */ + public static function add_private_dashboard_glance_items( $content ) { + add_filter( 'privacy_on_link_text', '__return_empty_string' ); + + return $content . + '

' . + wp_kses( + sprintf( + /* translators: URL for Jetpack dashboard. */ + __( 'This site is set to private. Make public.', 'jetpack' ), + esc_attr( 'jp-at-a-glance__site-private' ), + esc_url( admin_url( 'admin.php?page=jetpack#/security?term=private' ) ) + ), + array( + 'a' => array( 'href' => true ), + 'span' => array( 'class' => true ), + ) + ); + } + + /** + * Returns the private page template for OPML. + */ + public static function hide_opml() { + if ( is_user_logged_in() && self::is_private_blog_user( get_current_blog_id() ) ) { + return; + } + + wp_die( esc_html__( 'This site is private.', 'jetpack' ), 403 ); + } +} diff --git a/modules/private/private.php b/modules/private/private.php new file mode 100644 index 0000000000000..27c4ad3fda7c5 --- /dev/null +++ b/modules/private/private.php @@ -0,0 +1,53 @@ + + + + > + + + + +<?php echo bloginfo( 'name' ); ?> + + + + +
+

+
+

+
+
+ +

+
+ +
+ + diff --git a/yarn.lock b/yarn.lock index 3e52865608134..197fc963c89fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5643,7 +5643,7 @@ interpolate-components@1.1.1: dependencies: react "^0.14.3 || ^15.1.0 || ^16.0.0" react-addons-create-fragment "^0.14.3 || ^15.1.0" - react-dom "^0.14.3 || ^15.1.0 || ^16.0.0" + react-dom "^0.14.3 || ^15.1.0 || ^16.0.0" interpret@^1.1.0: version "1.2.0" @@ -9282,7 +9282,7 @@ rcloader@^0.2.2: loose-envify "^1.3.1" object-assign "^4.1.0" -react-dom@16.8.6, "react-dom@^0.14.3 || ^15.1.0 || ^16.0.0", react-dom@^16.8.4: +react-dom@16.8.6, "react-dom@^0.14.3 || ^15.1.0 || ^16.0.0", react-dom@^16.8.4: version "16.8.6" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" dependencies: @@ -11669,4 +11669,4 @@ yauzl@2.4.1: resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU= dependencies: - fd-slicer "~1.0.1" + fd-slicer "~1.0.1" \ No newline at end of file