From 6438a55e74ce512ad33b10f34e5b92e9b368c010 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Tue, 16 Apr 2024 16:13:52 +0200 Subject: [PATCH] SSO: add existing SSO classes to the Connection package. (#36587) * SSO: add classes to Connection package. * Add CSS and JS files * Fix enqueues * lowercase_p_dangit(); * changelog * Bring changes over from module * fix class references * Bump version * Add missing dependency * Address Phan warnings * Update Phan baseline * Update lock files * Update more Phan baselines * changelog * Bump versions * Fix body type * Add tests * Limit to Jetpack plugin for now The class still relies on Jetpack classes. * Try to fix tests * Fix test pollution * Bring in change from #36589 * Bring in changes from #36605 * Fix user invite box id reference * Update baseline for packages/backup * Bring in changes from #36690 * Update class reference See https://github.com/Automattic/jetpack/pull/36587#discussion_r1553324964 * Fix namespace See https://github.com/Automattic/jetpack/pull/36587#discussion_r155370971 * Update Phan config * Update Phan config again * More Phan config updates * changelog * Update projects/packages/connection/src/sso/class-sso.php Co-authored-by: Sergey Mitroshin * Bump versions * Ensure generated files are loaded properly * Bump versions * Fix asset generation and update to use Assets class * Bump version --------- Co-authored-by: Brad Jorsch Co-authored-by: sergeymitr --- projects/packages/backup/.phan/baseline.php | 4 +- .../changelog/add-sso-classes-connection | 5 + .../backup/src/class-package-version.php | 2 +- projects/packages/connection/.gitattributes | 12 +- .../packages/connection/.phan/baseline.php | 12 +- .../changelog/add-sso-classes-connection | 4 + projects/packages/connection/composer.json | 1 + .../packages/connection/src/class-client.php | 10 +- .../connection/src/class-package-version.php | 2 +- .../connection/src/sso/class-force-2fa.php | 182 +++ .../connection/src/sso/class-helpers.php | 436 ++++++ .../connection/src/sso/class-notices.php | 241 +++ .../packages/connection/src/sso/class-sso.php | 1267 ++++++++++++++++ .../connection/src/sso/class-user-admin.php | 1296 +++++++++++++++++ .../src/sso/jetpack-sso-admin-create-user.css | 21 + .../src/sso/jetpack-sso-admin-create-user.js | 45 + .../connection/src/sso/jetpack-sso-login.css | 164 +++ .../connection/src/sso/jetpack-sso-login.js | 27 + .../connection/src/sso/jetpack-sso-users.js | 12 + .../connection/tests/php/sso/test_Helpers.php | 416 ++++++ .../tests/php/test_Manager_integration.php | 12 +- .../tests/php/test_XMLPC_Async_Call.php | 16 + .../tests/php/test_jetpack_xmlrpc_server.php | 12 +- .../packages/connection/webpack.config.js | 25 +- projects/packages/forms/.phan/baseline.php | 4 +- .../changelog/add-sso-classes-connection | 5 + .../packages/my-jetpack/.phan/baseline.php | 6 +- .../changelog/add-sso-classes-connection | 5 + projects/packages/my-jetpack/package.json | 2 +- .../my-jetpack/src/class-initializer.php | 2 +- projects/packages/plans/.phan/baseline.php | 3 +- .../changelog/add-sso-classes-connection | 5 + projects/packages/plans/package.json | 2 +- .../packages/publicize/.phan/baseline.php | 4 +- .../changelog/add-sso-classes-connection | 5 + projects/packages/publicize/package.json | 2 +- .../changelog/add-sso-classes-connection | 5 + projects/packages/stats-admin/package.json | 2 +- .../packages/stats-admin/src/class-main.php | 2 +- .../changelog/add-sso-classes-connection | 5 + .../composer.lock | 3 +- .../changelog/add-sso-classes-connection | 5 + projects/plugins/backup/composer.lock | 3 +- .../changelog/add-sso-classes-connection | 5 + projects/plugins/boost/composer.lock | 3 +- .../changelog/add-sso-classes-connection | 5 + projects/plugins/inspect/composer.lock | 3 +- projects/plugins/jetpack/.phan/baseline.php | 17 +- .../changelog/add-sso-classes-connection | 5 + projects/plugins/jetpack/composer.lock | 3 +- projects/plugins/migration/.phan/baseline.php | 3 +- .../changelog/add-sso-classes-connection | 5 + projects/plugins/migration/composer.lock | 3 +- projects/plugins/protect/.phan/baseline.php | 2 +- .../changelog/add-sso-classes-connection | 5 + projects/plugins/protect/composer.lock | 3 +- .../changelog/add-sso-classes-connection | 5 + projects/plugins/search/composer.lock | 3 +- .../changelog/add-sso-classes-connection | 5 + projects/plugins/social/composer.lock | 3 +- .../changelog/add-sso-classes-connection | 5 + projects/plugins/starter-plugin/composer.lock | 3 +- .../changelog/add-sso-classes-connection | 5 + projects/plugins/videopress/composer.lock | 3 +- 64 files changed, 4329 insertions(+), 59 deletions(-) create mode 100644 projects/packages/backup/changelog/add-sso-classes-connection create mode 100644 projects/packages/connection/changelog/add-sso-classes-connection create mode 100644 projects/packages/connection/src/sso/class-force-2fa.php create mode 100644 projects/packages/connection/src/sso/class-helpers.php create mode 100644 projects/packages/connection/src/sso/class-notices.php create mode 100644 projects/packages/connection/src/sso/class-sso.php create mode 100644 projects/packages/connection/src/sso/class-user-admin.php create mode 100644 projects/packages/connection/src/sso/jetpack-sso-admin-create-user.css create mode 100644 projects/packages/connection/src/sso/jetpack-sso-admin-create-user.js create mode 100644 projects/packages/connection/src/sso/jetpack-sso-login.css create mode 100644 projects/packages/connection/src/sso/jetpack-sso-login.js create mode 100644 projects/packages/connection/src/sso/jetpack-sso-users.js create mode 100644 projects/packages/connection/tests/php/sso/test_Helpers.php create mode 100644 projects/packages/forms/changelog/add-sso-classes-connection create mode 100644 projects/packages/my-jetpack/changelog/add-sso-classes-connection create mode 100644 projects/packages/plans/changelog/add-sso-classes-connection create mode 100644 projects/packages/publicize/changelog/add-sso-classes-connection create mode 100644 projects/packages/stats-admin/changelog/add-sso-classes-connection create mode 100644 projects/plugins/automattic-for-agencies-client/changelog/add-sso-classes-connection create mode 100644 projects/plugins/backup/changelog/add-sso-classes-connection create mode 100644 projects/plugins/boost/changelog/add-sso-classes-connection create mode 100644 projects/plugins/inspect/changelog/add-sso-classes-connection create mode 100644 projects/plugins/jetpack/changelog/add-sso-classes-connection create mode 100644 projects/plugins/migration/changelog/add-sso-classes-connection create mode 100644 projects/plugins/protect/changelog/add-sso-classes-connection create mode 100644 projects/plugins/search/changelog/add-sso-classes-connection create mode 100644 projects/plugins/social/changelog/add-sso-classes-connection create mode 100644 projects/plugins/starter-plugin/changelog/add-sso-classes-connection create mode 100644 projects/plugins/videopress/changelog/add-sso-classes-connection diff --git a/projects/packages/backup/.phan/baseline.php b/projects/packages/backup/.phan/baseline.php index f264702d8a662..1c093993293f3 100644 --- a/projects/packages/backup/.phan/baseline.php +++ b/projects/packages/backup/.phan/baseline.php @@ -10,8 +10,8 @@ return [ // # Issue statistics: // PhanTypeMismatchReturnProbablyReal : 15+ occurrences - // PhanTypeMismatchArgumentProbablyReal : 8 occurrences // PhanTypeMismatchReturn : 6 occurrences + // PhanTypeMismatchArgumentProbablyReal : 3 occurrences // PhanUndeclaredStaticMethod : 2 occurrences // PhanPossiblyUndeclaredVariable : 1 occurrence // PhanUndeclaredClassMethod : 1 occurrence @@ -19,7 +19,7 @@ // Currently, file_suppressions and directory_suppressions are the only supported suppressions 'file_suppressions' => [ 'src/class-jetpack-backup.php' => ['PhanPossiblyUndeclaredVariable', 'PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredClassMethod', 'PhanUndeclaredStaticMethod'], - 'src/class-rest-controller.php' => ['PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal'], + 'src/class-rest-controller.php' => ['PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal'], ], // 'directory_suppressions' => ['src/directory_name' => ['PhanIssueName1', 'PhanIssueName2']] can be manually added if needed. // (directory_suppressions will currently be ignored by subsequent calls to --save-baseline, but may be preserved in future Phan releases) diff --git a/projects/packages/backup/changelog/add-sso-classes-connection b/projects/packages/backup/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..d88cd1b29112c --- /dev/null +++ b/projects/packages/backup/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Phan: update baseline files + + diff --git a/projects/packages/backup/src/class-package-version.php b/projects/packages/backup/src/class-package-version.php index e6420a315376b..cdab52b54a8e9 100644 --- a/projects/packages/backup/src/class-package-version.php +++ b/projects/packages/backup/src/class-package-version.php @@ -16,7 +16,7 @@ */ class Package_Version { - const PACKAGE_VERSION = '3.3.7'; + const PACKAGE_VERSION = '3.3.8-alpha'; const PACKAGE_SLUG = 'backup'; diff --git a/projects/packages/connection/.gitattributes b/projects/packages/connection/.gitattributes index ff566a9717cf7..d3c754b54ad84 100644 --- a/projects/packages/connection/.gitattributes +++ b/projects/packages/connection/.gitattributes @@ -10,8 +10,10 @@ tests/ export-ignore /dist/** production-include # Files not needed in the production build. -.phpcs.dir.xml production-exclude -/changelog/** production-exclude -.gitignore production-exclude -src/js/** production-exclude -webpack.config.js production-exclude +.phpcs.dir.xml production-exclude +/changelog/** production-exclude +.gitignore production-exclude +src/js/** production-exclude +webpack.config.js production-exclude +/dist/*.css.map production-exclude +/dist/*.js.map production-exclude diff --git a/projects/packages/connection/.phan/baseline.php b/projects/packages/connection/.phan/baseline.php index b8fee6e905a7b..dc016b828aa5f 100644 --- a/projects/packages/connection/.phan/baseline.php +++ b/projects/packages/connection/.phan/baseline.php @@ -12,19 +12,19 @@ // PhanTypeMismatchArgument : 55+ occurrences // PhanParamTooMany : 40+ occurrences // PhanUndeclaredMethod : 35+ occurrences + // PhanTypeMismatchArgumentProbablyReal : 20+ occurrences // PhanDeprecatedFunction : 15+ occurrences // PhanPluginDuplicateConditionalNullCoalescing : 15+ occurrences - // PhanTypeMismatchArgumentProbablyReal : 15+ occurrences // PhanTypeMismatchReturn : 15+ occurrences // PhanUndeclaredClassMethod : 15+ occurrences // PhanTypeMismatchProperty : 9 occurrences // PhanTypeMismatchPropertyProbablyReal : 9 occurrences + // PhanNoopNew : 8 occurrences // PhanTypeMismatchReturnProbablyReal : 8 occurrences // PhanUndeclaredProperty : 8 occurrences - // PhanNoopNew : 6 occurrences + // PhanRedundantCondition : 5 occurrences // PhanTypeArraySuspiciousNullable : 5 occurrences // PhanTypeMismatchDefault : 5 occurrences - // PhanRedundantCondition : 4 occurrences // PhanTypeMismatchArgumentInternal : 4 occurrences // PhanTypeMismatchArgumentNullable : 4 occurrences // PhanTypeObjectUnsetDeclaredProperty : 3 occurrences @@ -33,6 +33,7 @@ // PhanCommentParamWithoutRealParam : 2 occurrences // PhanImpossibleCondition : 2 occurrences // PhanNonClassMethodCall : 2 occurrences + // PhanPluginUnreachableCode : 2 occurrences // PhanPossiblyUndeclaredVariable : 2 occurrences // PhanTypeMismatchPropertyDefault : 2 occurrences // PhanTypeMismatchReturnNullable : 2 occurrences @@ -72,10 +73,13 @@ 'src/class-secrets.php' => ['PhanCommentParamWithoutRealParam', 'PhanNonClassMethodCall', 'PhanTypeMismatchArgument'], 'src/class-server-sandbox.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanTypeMismatchArgument'], 'src/class-tokens.php' => ['PhanImpossibleTypeComparison', 'PhanTypeMismatchArgumentInternal', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal'], - 'src/class-tracking.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchDefault', 'PhanTypePossiblyInvalidDimOffset', 'PhanUndeclaredClassMethod'], + 'src/class-tracking.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchDefault', 'PhanTypePossiblyInvalidDimOffset'], 'src/class-urls.php' => ['PhanTypeSuspiciousStringExpression', 'PhanUndeclaredFunctionInCallable'], 'src/class-webhooks.php' => ['PhanTypeMismatchArgumentProbablyReal'], 'src/class-xmlrpc-connector.php' => ['PhanUndeclaredTypeReturnType'], + 'src/sso/class-helpers.php' => ['PhanTypeMismatchArgumentProbablyReal'], + 'src/sso/class-sso.php' => ['PhanNoopNew', 'PhanRedundantCondition', 'PhanTypeMismatchArgument', 'PhanTypeMismatchArgumentProbablyReal', 'PhanUndeclaredClassMethod'], + 'src/sso/class-user-admin.php' => ['PhanPluginUnreachableCode', 'PhanTypeMismatchArgument'], 'src/webhooks/class-authorize-redirect.php' => ['PhanTypeMismatchArgumentNullable', 'PhanTypeMismatchProperty', 'PhanUndeclaredClassMethod', 'PhanUndeclaredClassReference', 'PhanUndeclaredTypeProperty'], 'tests/php/test-class-nonce-handler.php' => ['PhanPluginDuplicateAdjacentStatement', 'PhanTypeMismatchArgument'], 'tests/php/test-class-plugin.php' => ['PhanUndeclaredTypeThrowsType'], diff --git a/projects/packages/connection/changelog/add-sso-classes-connection b/projects/packages/connection/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..bc6d35f337e79 --- /dev/null +++ b/projects/packages/connection/changelog/add-sso-classes-connection @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +SSO: add SSO feature to the package. diff --git a/projects/packages/connection/composer.json b/projects/packages/connection/composer.json index 35b2e827a01de..711e94ed95e5e 100644 --- a/projects/packages/connection/composer.json +++ b/projects/packages/connection/composer.json @@ -7,6 +7,7 @@ "php": ">=7.0", "automattic/jetpack-a8c-mc-stats": "@dev", "automattic/jetpack-admin-ui": "@dev", + "automattic/jetpack-assets": "@dev", "automattic/jetpack-constants": "@dev", "automattic/jetpack-roles": "@dev", "automattic/jetpack-status": "@dev", diff --git a/projects/packages/connection/src/class-client.php b/projects/packages/connection/src/class-client.php index 2167ff00a0fea..6db15a3903d1c 100644 --- a/projects/packages/connection/src/class-client.php +++ b/projects/packages/connection/src/class-client.php @@ -408,11 +408,11 @@ public static function validate_args_for_wpcom_json_api_request( /** * Queries the WordPress.com REST API with a user token. * - * @param string $path REST API path. - * @param string $version REST API version. Default is `2`. - * @param array $args Arguments to {@see WP_Http}. Default is `array()`. - * @param string $body Body passed to {@see WP_Http}. Default is `null`. - * @param string $base_api_path REST API root. Default is `wpcom`. + * @param string $path REST API path. + * @param string $version REST API version. Default is `2`. + * @param array $args Arguments to {@see WP_Http}. Default is `array()`. + * @param null|string|array $body Body passed to {@see WP_Http}. Default is `null`. + * @param string $base_api_path REST API root. Default is `wpcom`. * * @return array|WP_Error $response Response data, else {@see WP_Error} on failure. */ diff --git a/projects/packages/connection/src/class-package-version.php b/projects/packages/connection/src/class-package-version.php index b9f3ed5d3ecc7..3aee708c5c243 100644 --- a/projects/packages/connection/src/class-package-version.php +++ b/projects/packages/connection/src/class-package-version.php @@ -12,7 +12,7 @@ */ class Package_Version { - const PACKAGE_VERSION = '2.7.1'; + const PACKAGE_VERSION = '2.7.2-alpha'; const PACKAGE_SLUG = 'connection'; diff --git a/projects/packages/connection/src/sso/class-force-2fa.php b/projects/packages/connection/src/sso/class-force-2fa.php new file mode 100644 index 0000000000000..61fb4ffcc7dc9 --- /dev/null +++ b/projects/packages/connection/src/sso/class-force-2fa.php @@ -0,0 +1,182 @@ +role = apply_filters( 'jetpack_force_2fa_cap', 'manage_options' ); + + // Bail if Jetpack SSO is not active + if ( + ! class_exists( 'Jetpack' ) + || ! ( new Modules() )->is_active( 'sso' ) + ) { + add_action( 'admin_notices', array( $this, 'admin_notice' ) ); + return; + } + + $this->force_2fa(); + } + + /** + * Display an admin notice if Jetpack SSO is not active. + */ + public function admin_notice() { + /** + * Filter if an admin notice is deplayed when Force 2FA is required, but SSO is not enabled. + * Defaults to true. + * + * @param bool $display_notice Whether to display the notice. + * @return bool + * @since jetpack-12.7 + * @module SSO + */ + if ( apply_filters( 'jetpack_force_2fa_dependency_notice', true ) && current_user_can( $this->role ) ) { + printf( '

%2$s

', 'notice notice-warning', 'Jetpack Force 2FA requires Jetpack and the Jetpack SSO module.' ); + } + } + + /** + * Force 2FA when using Jetpack SSO and force Jetpack SSO. + * + * @return void + */ + private function force_2fa() { + // Allows WP.com login to a local account if it matches the local account. + add_filter( 'jetpack_sso_match_by_email', '__return_true', 9999 ); + + // multisite + if ( is_multisite() ) { + + // Hide the login form + add_filter( 'jetpack_remove_login_form', '__return_true', 9999 ); + add_filter( 'jetpack_sso_bypass_login_forward_wpcom', '__return_true', 9999 ); + add_filter( 'jetpack_sso_display_disclaimer', '__return_false', 9999 ); + + add_filter( + 'wp_authenticate_user', + function () { + return new WP_Error( 'wpcom-required', $this->get_login_error_message() ); }, + 9999 + ); + + add_filter( 'jetpack_sso_require_two_step', '__return_true' ); + + add_filter( 'allow_password_reset', '__return_false' ); + } else { + // Not multisite. + + // Completely disable the standard login form for admins. + add_filter( + 'wp_authenticate_user', + function ( $user ) { + if ( is_wp_error( $user ) ) { + return $user; + } + if ( $user->has_cap( $this->role ) ) { + return new WP_Error( 'wpcom-required', $this->get_login_error_message(), $user->user_login ); + } + return $user; + }, + 9999 + ); + + add_filter( + 'allow_password_reset', + function ( $allow, $user_id ) { + if ( user_can( $user_id, $this->role ) ) { + return false; + } + return $allow; }, + 9999, + 2 + ); + + add_action( 'jetpack_sso_pre_handle_login', array( $this, 'jetpack_set_two_step' ) ); + } + } + + /** + * Specifically set the two step filter for Jetpack SSO. + * + * @param Object $user_data The user data from WordPress.com. + * + * @return void + */ + public function jetpack_set_two_step( $user_data ) { + $user = SSO::get_user_by_wpcom_id( $user_data->ID ); + + // Borrowed from Jetpack. Ignores the match_by_email setting. + if ( empty( $user ) ) { + $user = get_user_by( 'email', $user_data->email ); + } + + if ( $user && $user->has_cap( $this->role ) ) { + add_filter( 'jetpack_sso_require_two_step', '__return_true' ); + } + } + + /** + * Get the login error message. + * + * @return string + */ + private function get_login_error_message() { + /** + * Filter the login error message. + * Defaults to a message that explains the user must use a WordPress.com account with 2FA enabled. + * + * @param string $message The login error message. + * @return string + * @since jetpack-12.7 + * @module SSO + */ + return apply_filters( + 'jetpack_force_2fa_login_error_message', + sprintf( 'For added security, please log in using your WordPress.com account.

Note: Your account must have Two Step Authentication enabled, which can be configured from Security Settings.', 'https://support.wordpress.com/security/two-step-authentication/', 'https://wordpress.com/me/security/two-step' ) + ); + } +} diff --git a/projects/packages/connection/src/sso/class-helpers.php b/projects/packages/connection/src/sso/class-helpers.php new file mode 100644 index 0000000000000..200e16e83d04e --- /dev/null +++ b/projects/packages/connection/src/sso/class-helpers.php @@ -0,0 +1,436 @@ +login; + /** + * Determines how many times the SSO module can attempt to randomly generate a user. + * + * @module sso + * + * @since jetpack-4.3.2 + * + * @param int 5 By default, SSO will attempt to random generate a user up to 5 times. + */ + $num_tries = (int) apply_filters( 'jetpack_sso_allowed_username_generate_retries', 5 ); + + $exists = username_exists( $username ); + $tries = 0; + while ( $exists && $tries++ < $num_tries ) { + $username = $user_data->login . '_' . $user_data->ID . '_' . wp_rand(); + $exists = username_exists( $username ); + } + + if ( $exists ) { + return false; + } + + $user = (object) array(); + $user->user_pass = wp_generate_password( 20 ); + $user->user_login = wp_slash( $username ); + $user->user_email = wp_slash( $user_data->email ); + $user->display_name = $user_data->display_name; + $user->first_name = $user_data->first_name; + $user->last_name = $user_data->last_name; + $user->url = $user_data->url; + $user->description = $user_data->description; + + if ( isset( $user_data->role ) && $user_data->role ) { + $user->role = $user_data->role; + } + + $created_user_id = wp_insert_user( $user ); + + update_user_meta( $created_user_id, 'wpcom_user_id', $user_data->ID ); + return get_userdata( $created_user_id ); + } + + /** + * Determines how long the auth cookie is valid for when a user logs in with SSO. + * + * @return int result of the jetpack_sso_auth_cookie_expiration filter. + */ + public static function extend_auth_cookie_expiration_for_sso() { + /** + * Determines how long the auth cookie is valid for when a user logs in with SSO. + * + * @module sso + * + * @since jetpack-4.4.0 + * @since jetpack-6.1.0 Fixed a typo. Filter was previously jetpack_sso_auth_cookie_expirtation. + * + * @param int YEAR_IN_SECONDS + */ + return (int) apply_filters( 'jetpack_sso_auth_cookie_expiration', YEAR_IN_SECONDS ); + } + + /** + * Determines if the SSO form should be displayed for the current action. + * + * @since jetpack-4.6.0 + * + * @param string $action SSO action being performed. + * + * @return bool Is SSO allowed for the current action? + */ + public static function display_sso_form_for_action( $action ) { + /** + * Allows plugins the ability to overwrite actions where the SSO form is allowed to be used. + * + * @module sso + * + * @since jetpack-4.6.0 + * + * @param array $allowed_actions_for_sso + */ + $allowed_actions_for_sso = (array) apply_filters( + 'jetpack_sso_allowed_actions', + array( + 'login', + 'jetpack-sso', + 'jetpack_json_api_authorization', + ) + ); + return in_array( $action, $allowed_actions_for_sso, true ); + } + + /** + * This method returns an environment array that is meant to simulate `$_REQUEST` when the initial + * JSON API auth request was made. + * + * @since jetpack-4.6.0 + * + * @return array|bool + */ + public static function get_json_api_auth_environment() { + if ( empty( $_COOKIE['jetpack_sso_original_request'] ) ) { + return false; + } + + $original_request = esc_url_raw( wp_unslash( $_COOKIE['jetpack_sso_original_request'] ) ); + + $parsed_url = wp_parse_url( $original_request ); + if ( empty( $parsed_url ) || empty( $parsed_url['query'] ) ) { + return false; + } + + $args = array(); + wp_parse_str( $parsed_url['query'], $args ); + + if ( empty( $args ) || empty( $args['action'] ) ) { + return false; + } + + if ( 'jetpack_json_api_authorization' !== $args['action'] ) { + return false; + } + + return array_merge( + $args, + array( 'jetpack_json_api_original_query' => $original_request ) + ); + } + + /** + * Check if the site has a custom login page URL, and return it. + * If default login page URL is used (`wp-login.php`), `null` will be returned. + * + * @return string|null + */ + public static function get_custom_login_url() { + $login_url = wp_login_url(); + + if ( str_ends_with( $login_url, 'wp-login.php' ) ) { + // No custom URL found. + return null; + } + + $site_url = trailingslashit( site_url() ); + + if ( ! str_starts_with( $login_url, $site_url ) ) { + // Something went wrong, we can't properly extract the custom URL. + return null; + } + + // Extracting the "path" part of the URL, because we don't need the `site_url` part. + return str_ireplace( $site_url, '', $login_url ); + } + + /** + * Clear the cookies that store the profile information for the last + * WPCOM user to connect. + */ + public static function clear_wpcom_profile_cookies() { + if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] ) ) { + setcookie( + 'jetpack_sso_wpcom_name_' . COOKIEHASH, + ' ', + time() - YEAR_IN_SECONDS, + COOKIEPATH, + COOKIE_DOMAIN, + is_ssl(), + true + ); + } + + if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] ) ) { + setcookie( + 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH, + ' ', + time() - YEAR_IN_SECONDS, + COOKIEPATH, + COOKIE_DOMAIN, + is_ssl(), + true + ); + } + } + + /** + * Remove an SSO connection for a user. + * + * @param int $user_id The local user id. + */ + public static function delete_connection_for_user( $user_id ) { + $wpcom_user_id = get_user_meta( $user_id, 'wpcom_user_id', true ); + if ( ! $wpcom_user_id ) { + return; + } + + $xml = new Jetpack_IXR_Client( + array( + 'wpcom_user_id' => $user_id, + ) + ); + $xml->query( 'jetpack.sso.removeUser', $wpcom_user_id ); + + if ( $xml->isError() ) { + return false; + } + + // Clean up local data stored for SSO. + delete_user_meta( $user_id, 'wpcom_user_id' ); + delete_user_meta( $user_id, 'wpcom_user_data' ); + self::clear_wpcom_profile_cookies(); + + return $xml->getResponse(); + } +} diff --git a/projects/packages/connection/src/sso/class-notices.php b/projects/packages/connection/src/sso/class-notices.php new file mode 100644 index 0000000000000..ab3f8aed2c477 --- /dev/null +++ b/projects/packages/connection/src/sso/class-notices.php @@ -0,0 +1,241 @@ +Security Settings to configure Two-step Authentication for your account.', + 'jetpack-connection' + ), + array( 'a' => array( 'href' => array() ) ) + ), + Redirect::get_url( 'calypso-me-security-two-step' ), + Redirect::get_url( 'wpcom-support-security-two-step-authentication' ) + ); + + $message .= sprintf( '

%s

', $error ); + + return $message; + } + + /** + * Error message displayed when the user tries to SSO, but match by email + * is off and they already have an account with their email address on + * this site. + * + * @param string $message Error message. + * @return string + */ + public static function error_msg_email_already_exists( $message ) { + $error = sprintf( + wp_kses( + /* translators: login URL */ + __( + 'You already have an account on this site. Please sign in with your username and password and then connect to WordPress.com.', + 'jetpack-connection' + ), + array( 'a' => array( 'href' => array() ) ) + ), + esc_url_raw( add_query_arg( 'jetpack-sso-show-default-form', '1', wp_login_url() ) ) + ); + + $message .= sprintf( '

%s

', $error ); + + return $message; + } + + /** + * Error message that is displayed when the current site is in an identity crisis and SSO can not be used. + * + * @since jetpack-4.3.2 + * + * @param string $message Error Message. + * + * @return string + */ + public static function error_msg_identity_crisis( $message ) { + $error = esc_html__( 'Logging in with WordPress.com is not currently available because this site is experiencing connection problems.', 'jetpack-connection' ); + $message .= sprintf( '

%s

', $error ); + return $message; + } + + /** + * Error message that is displayed when we are not able to verify the SSO nonce due to an XML error or + * failed validation. In either case, we prompt the user to try again or log in with username and password. + * + * @since jetpack-4.3.2 + * + * @param string $message Error message. + * + * @return string + */ + public static function error_invalid_response_data( $message ) { + $error = esc_html__( + 'There was an error logging you in via WordPress.com, please try again or try logging in with your username and password.', + 'jetpack-connection' + ); + $message .= sprintf( '

%s

', $error ); + return $message; + } + + /** + * Error message that is displayed when we were not able to automatically create an account for a user + * after a user has logged in via SSO. By default, this message is triggered after trying to create an account 5 times. + * + * @since jetpack-4.3.2 + * + * @param string $message Error message. + * + * @return string + */ + public static function error_unable_to_create_user( $message ) { + $error = esc_html__( + 'There was an error creating a user for you. Please contact the administrator of your site.', + 'jetpack-connection' + ); + $message .= sprintf( '

%s

', $error ); + return $message; + } + + /** + * When the default login form is hidden, this method is called on the 'authenticate' filter with a priority of 30. + * This method disables the ability to submit the default login form. + * + * @param WP_User|WP_Error $user Either the user attempting to login or an existing authentication failure. + * + * @return WP_Error + */ + public static function disable_default_login_form( $user ) { + if ( is_wp_error( $user ) ) { + return $user; + } + + /** + * Since we're returning an error that will be shown as a red notice, let's remove the + * informational "blue" notice. + */ + remove_filter( 'login_message', array( static::class, 'msg_login_by_jetpack' ) ); + return new WP_Error( 'jetpack_sso_required', self::get_sso_required_message() ); + } + + /** + * Message displayed when the site admin has disabled the default WordPress + * login form in Settings > General > Secure Sign On + * + * @since jetpack-2.7 + * @param string $message Error message. + * + * @return string + **/ + public static function msg_login_by_jetpack( $message ) { + $message .= sprintf( '

%s

', self::get_sso_required_message() ); + return $message; + } + + /** + * Get the message for SSO required. + * + * @return string + */ + public static function get_sso_required_message() { + $msg = esc_html__( + 'A WordPress.com account is required to access this site. Click the button below to sign in or create a free WordPress.com account.', + 'jetpack-connection' + ); + + /** + * Filter the message displayed when the default WordPress login form is disabled. + * + * @module sso + * + * @since jetpack-2.8.0 + * + * @param string $msg Disclaimer when default WordPress login form is disabled. + */ + return apply_filters( 'jetpack_sso_disclaimer_message', $msg ); + } + + /** + * Message displayed when the user can not be found after approving the SSO process on WordPress.com + * + * @param string $message Error message. + * + * @return string + */ + public static function cant_find_user( $message ) { + $error = __( + "We couldn't find your account. If you already have an account, make sure you have connected to WordPress.com.", + 'jetpack-connection' + ); + + /** + * Filters the "couldn't find your account" notice after an attempted SSO. + * + * @module sso + * + * @since jetpack-10.5.0 + * + * @param string $error Error text. + */ + $error = apply_filters( 'jetpack_sso_unknown_user_notice', $error ); + + $message .= sprintf( '

%s

', esc_html( $error ) ); + + return $message; + } + + /** + * Error message that is displayed when the current site is in an identity crisis and SSO can not be used. + * + * @since jetpack-4.4.0 + * + * @param string $message Error message. + * + * @return string + */ + public static function sso_not_allowed_in_staging( $message ) { + $error = __( + 'Logging in with WordPress.com is disabled for sites that are in staging mode.', + 'jetpack-connection' + ); + + /** + * Filters the disallowed notice for staging sites attempting SSO. + * + * @module sso + * + * @since jetpack-10.5.0 + * + * @param string $error Error text. + */ + $error = apply_filters( 'jetpack_sso_disallowed_staging_notice', $error ); + $message .= sprintf( '

%s

', esc_html( $error ) ); + return $message; + } +} diff --git a/projects/packages/connection/src/sso/class-sso.php b/projects/packages/connection/src/sso/class-sso.php new file mode 100644 index 0000000000000..783d1ae67ee2a --- /dev/null +++ b/projects/packages/connection/src/sso/class-sso.php @@ -0,0 +1,1267 @@ +is_user_connected() && + ! is_multisite() && + /** + * Toggle the ability to invite new users to create a WordPress.com account. + * + * @module sso + * + * @since $$next-version$$ + * + * @param bool true Whether to allow admins to invite new users to create a WordPress.com account. + */ + apply_filters( 'jetpack_sso_invite_new_users_wpcom', true ) + ) { + new User_Admin(); + } + } + + /** + * Returns the single instance of the Automattic\Jetpack\Connection\SSO object + * + * @since jetpack-2.8 + * @return \Automattic\Jetpack\Connection\SSO + */ + public static function get_instance() { + if ( self::$instance !== null ) { + return self::$instance; + } + + self::$instance = new SSO(); + return self::$instance; + } + + /** + * Safety heads-up added to the logout messages when SSO is enabled. + * Some folks on a shared computer don't know that they need to log out of WordPress.com as well. + * + * @param WP_Error $errors WP_Error object. + */ + public function sso_reminder_logout_wpcom( $errors ) { + if ( ( new Host() )->is_wpcom_platform() ) { + return $errors; + } + + if ( ! empty( $errors->errors['loggedout'] ) ) { + $logout_message = wp_kses( + sprintf( + /* translators: %1$s is a link to the WordPress.com account settings page. */ + __( 'If you are on a shared computer, remember to also log out of WordPress.com.', 'jetpack-connection' ), + 'https://wordpress.com/me' + ), + array( + 'a' => array( + 'href' => array(), + ), + ) + ); + $errors->add( 'jetpack-sso-show-logout', $logout_message, 'message' ); + } + return $errors; + } + + /** + * If jetpack_force_logout == 1 in current user meta the user will be forced + * to logout and reauthenticate with the site. + **/ + public function maybe_logout_user() { + global $current_user; + + if ( 1 === (int) $current_user->jetpack_force_logout ) { + delete_user_meta( $current_user->ID, 'jetpack_force_logout' ); + Helpers::delete_connection_for_user( $current_user->ID ); + wp_logout(); + wp_safe_redirect( wp_login_url() ); + exit; + } + } + + /** + * Adds additional methods the WordPress xmlrpc API for handling SSO specific features + * + * @param array $methods API methods. + * @return array + **/ + public function xmlrpc_methods( $methods ) { + $methods['jetpack.userDisconnect'] = array( $this, 'xmlrpc_user_disconnect' ); + return $methods; + } + + /** + * Marks a user's profile for disconnect from WordPress.com and forces a logout + * the next time the user visits the site. + * + * @param int $user_id User to disconnect from the site. + **/ + public function xmlrpc_user_disconnect( $user_id ) { + $user_query = new WP_User_Query( + array( + 'meta_key' => 'wpcom_user_id', + 'meta_value' => $user_id, + ) + ); + $user = $user_query->get_results(); + $user = $user[0]; + + if ( $user instanceof WP_User ) { + $user = wp_set_current_user( $user->ID ); + update_user_meta( $user->ID, 'jetpack_force_logout', '1' ); + Helpers::delete_connection_for_user( $user->ID ); + return true; + } + return false; + } + + /** + * Enqueues scripts and styles necessary for SSO login. + */ + public function login_enqueue_scripts() { + global $action; + + if ( ! Helpers::display_sso_form_for_action( $action ) ) { + return; + } + + Assets::register_script( + 'jetpack-sso-login', + '../../dist/jetpack-sso-login.js', + __FILE__, + array( + 'enqueue' => true, + 'version' => Package_Version::PACKAGE_VERSION, + ) + ); + } + + /** + * Adds Jetpack SSO classes to login body + * + * @param array $classes Array of classes to add to body tag. + * @return array Array of classes to add to body tag. + */ + public function login_body_class( $classes ) { + global $action; + + if ( ! Helpers::display_sso_form_for_action( $action ) ) { + return $classes; + } + + // Always add the jetpack-sso class so that we can add SSO specific styling even when the SSO form isn't being displayed. + $classes[] = 'jetpack-sso'; + + if ( ! ( new Status() )->is_staging_site() ) { + /** + * Should we show the SSO login form? + * + * $_GET['jetpack-sso-default-form'] is used to provide a fallback in case JavaScript is not enabled. + * + * The default_to_sso_login() method allows us to dynamically decide whether we show the SSO login form or not. + * The SSO module uses the method to display the default login form if we can not find a user to log in via SSO. + * But, the method could be filtered by a site admin to always show the default login form if that is preferred. + */ + if ( empty( $_GET['jetpack-sso-show-default-form'] ) && Helpers::show_sso_login() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $classes[] = 'jetpack-sso-form-display'; + } + } + + return $classes; + } + + /** + * Inlined admin styles for SSO. + */ + public function print_inline_admin_css() { + ?> + + General > Secure Sign On that allows users to + * turn off the login form on wp-login.php + * + * @since jetpack-2.7 + **/ + public function register_settings() { + + add_settings_section( + 'jetpack_sso_settings', + __( 'Secure Sign On', 'jetpack-connection' ), + '__return_false', + 'jetpack-sso' + ); + + /* + * Settings > General > Secure Sign On + * Require two step authentication + */ + register_setting( + 'jetpack-sso', + 'jetpack_sso_require_two_step', + array( $this, 'validate_jetpack_sso_require_two_step' ) + ); + + add_settings_field( + 'jetpack_sso_require_two_step', + '', // Output done in render $callback: __( 'Require Two-Step Authentication' , 'jetpack-connection' ). + array( $this, 'render_require_two_step' ), + 'jetpack-sso', + 'jetpack_sso_settings' + ); + + /* + * Settings > General > Secure Sign On + */ + register_setting( + 'jetpack-sso', + 'jetpack_sso_match_by_email', + array( $this, 'validate_jetpack_sso_match_by_email' ) + ); + + add_settings_field( + 'jetpack_sso_match_by_email', + '', // Output done in render $callback: __( 'Match by Email' , 'jetpack-connection' ). + array( $this, 'render_match_by_email' ), + 'jetpack-sso', + 'jetpack_sso_settings' + ); + } + + /** + * Builds the display for the checkbox allowing user to require two step + * auth be enabled on WordPress.com accounts before login. Displays in Settings > General + * + * @since jetpack-2.7 + **/ + public function render_require_two_step() { + ?> + + General. + * + * @param bool $input The jetpack_sso_require_two_step option setting. + * + * @since jetpack-2.7 + * @return int + **/ + public function validate_jetpack_sso_require_two_step( $input ) { + return ( ! empty( $input ) ) ? 1 : 0; + } + + /** + * Builds the display for the checkbox allowing the user to allow matching logins by email + * Displays in Settings > General + * + * @since jetpack-2.9 + **/ + public function render_match_by_email() { + ?> + + General. + * + * @param bool $input The jetpack_sso_match_by_email option setting. + * + * @since jetpack-2.9 + * @return int + **/ + public function validate_jetpack_sso_match_by_email( $input ) { + return ( ! empty( $input ) ) ? 1 : 0; + } + + /** + * Checks to determine if the user wants to login on wp-login + * + * This function mostly exists to cover the exceptions to login + * that may exist as other parameters to $_GET[action] as $_GET[action] + * does not have to exist. By default WordPress assumes login if an action + * is not set, however this may not be true, as in the case of logout + * where $_GET[loggedout] is instead set + * + * @return boolean + **/ + private function wants_to_login() { + $wants_to_login = false; + + // Cover default WordPress behavior. + $action = isset( $_REQUEST['action'] ) ? filter_var( wp_unslash( $_REQUEST['action'] ) ) : 'login'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + // And now the exceptions. + $action = isset( $_GET['loggedout'] ) ? 'loggedout' : $action; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( Helpers::display_sso_form_for_action( $action ) ) { + $wants_to_login = true; + } + + return $wants_to_login; + } + + /** + * Checks to determine if the user has indicated they want to use the wp-admin interface. + */ + private function use_wp_admin_interface() { + return 'wp-admin' === get_option( 'wpcom_admin_interface' ); + } + + /** + * Initialization for a SSO request. + */ + public function login_init() { + global $action; + + $tracking = new Tracking(); + + if ( Helpers::should_hide_login_form() ) { + /** + * Since the default authenticate filters fire at priority 20 for checking username and password, + * let's fire at priority 30. wp_authenticate_spam_check is fired at priority 99, but since we return a + * WP_Error in disable_default_login_form, then we won't trigger spam processing logic. + */ + add_filter( 'authenticate', array( Notices::class, 'disable_default_login_form' ), 30 ); + + /** + * Filter the display of the disclaimer message appearing when default WordPress login form is disabled. + * + * @module sso + * + * @since jetpack-2.8.0 + * + * @param bool true Should the disclaimer be displayed. Default to true. + */ + $display_sso_disclaimer = apply_filters( 'jetpack_sso_display_disclaimer', true ); + if ( $display_sso_disclaimer ) { + add_filter( 'login_message', array( Notices::class, 'msg_login_by_jetpack' ) ); + } + } + + if ( 'jetpack-sso' === $action ) { + if ( isset( $_GET['result'] ) && isset( $_GET['user_id'] ) && isset( $_GET['sso_nonce'] ) && 'success' === $_GET['result'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $this->handle_login(); + $this->display_sso_login_form(); + } elseif ( ( new Status() )->is_staging_site() ) { + add_filter( 'login_message', array( Notices::class, 'sso_not_allowed_in_staging' ) ); + } else { + // Is it wiser to just use wp_redirect than do this runaround to wp_safe_redirect? + add_filter( 'allowed_redirect_hosts', array( Helpers::class, 'allowed_redirect_hosts' ) ); + $reauth = ! empty( $_GET['force_reauth'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $sso_url = $this->get_sso_url_or_die( $reauth ); + + $tracking->record_user_event( 'sso_login_redirect_success' ); + wp_safe_redirect( $sso_url ); + exit; + } + } elseif ( Helpers::display_sso_form_for_action( $action ) ) { + + // Save cookies so we can handle redirects after SSO. + static::save_cookies(); + + /** + * Check to see if the site admin wants to automagically forward the user + * to the WordPress.com login page AND that the request to wp-login.php + * is not something other than login (Like logout!) + */ + if ( ! $this->use_wp_admin_interface() && Helpers::bypass_login_forward_wpcom() && $this->wants_to_login() ) { + add_filter( 'allowed_redirect_hosts', array( Helpers::class, 'allowed_redirect_hosts' ) ); + $reauth = ! empty( $_GET['force_reauth'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $sso_url = $this->get_sso_url_or_die( $reauth ); + $tracking->record_user_event( 'sso_login_redirect_bypass_success' ); + wp_safe_redirect( $sso_url ); + exit; + } + + $this->display_sso_login_form(); + } + } + + /** + * Ensures that we can get a nonce from WordPress.com via XML-RPC before setting + * up the hooks required to display the SSO form. + */ + public function display_sso_login_form() { + add_filter( 'login_body_class', array( $this, 'login_body_class' ) ); + add_action( 'login_head', array( $this, 'print_inline_admin_css' ) ); + + if ( ( new Status() )->is_staging_site() ) { + add_filter( 'login_message', array( Notices::class, 'sso_not_allowed_in_staging' ) ); + return; + } + + $sso_nonce = self::request_initial_nonce(); + if ( is_wp_error( $sso_nonce ) ) { + return; + } + + add_action( 'login_form', array( $this, 'login_form' ) ); + add_action( 'login_enqueue_scripts', array( $this, 'login_enqueue_scripts' ) ); + } + + /** + * Conditionally save the redirect_to url as a cookie. + * + * @since jetpack-4.6.0 Renamed to save_cookies from maybe_save_redirect_cookies + */ + public static function save_cookies() { + if ( headers_sent() ) { + return new WP_Error( 'headers_sent', __( 'Cannot deal with cookie redirects, as headers are already sent.', 'jetpack-connection' ) ); + } + + setcookie( + 'jetpack_sso_original_request', + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sniff misses the wrapping esc_url_raw(). + esc_url_raw( set_url_scheme( ( isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : '' ) . ( isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '' ) ) ), + time() + HOUR_IN_SECONDS, + COOKIEPATH, + COOKIE_DOMAIN, + is_ssl(), + true + ); + + if ( ! empty( $_GET['redirect_to'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + // If we have something to redirect to. + $url = esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + setcookie( 'jetpack_sso_redirect_to', $url, time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); + } elseif ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) { + // Otherwise, if it's already set, purge it. + setcookie( 'jetpack_sso_redirect_to', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); + } + } + + /** + * Outputs the Jetpack SSO button and description as well as the toggle link + * for switching between Jetpack SSO and default login. + */ + public function login_form() { + $site_name = get_bloginfo( 'name' ); + if ( ! $site_name ) { + $site_name = get_bloginfo( 'url' ); + } + + $display_name = ! empty( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] ) + ? sanitize_text_field( wp_unslash( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] ) ) + : false; + $gravatar = ! empty( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] ) + ? esc_url_raw( wp_unslash( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] ) ) + : false; + + ?> +
+ +
+ + +

+ %s', 'jetpack-connection' ), esc_html( $display_name ) ), + array( 'span' => true ) + ); + ?> +

+
+ + + + +
+ build_sso_button( array(), true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaping done in build_sso_button() ?> + + + + + + +

+ +

+ +
+ + +
+ +
+ + + + + + + + + +
+ is_user_connected() ) { + Helpers::delete_connection_for_user( get_current_user_id() ); + } + } + + /** + * Retrieves nonce used for SSO form. + * + * @return string|WP_Error + */ + public static function request_initial_nonce() { + $nonce = ! empty( $_COOKIE['jetpack_sso_nonce'] ) + ? sanitize_key( wp_unslash( $_COOKIE['jetpack_sso_nonce'] ) ) + : false; + + if ( ! $nonce ) { + $xml = new Jetpack_IXR_Client(); + $xml->query( 'jetpack.sso.requestNonce' ); + + if ( $xml->isError() ) { + return new WP_Error( $xml->getErrorCode(), $xml->getErrorMessage() ); + } + + $nonce = sanitize_key( $xml->getResponse() ); + + setcookie( + 'jetpack_sso_nonce', + $nonce, + time() + ( 10 * MINUTE_IN_SECONDS ), + COOKIEPATH, + COOKIE_DOMAIN, + is_ssl(), + true + ); + } + + return $nonce; + } + + /** + * The function that actually handles the login! + */ + public function handle_login() { + $wpcom_nonce = isset( $_GET['sso_nonce'] ) ? sanitize_key( $_GET['sso_nonce'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $wpcom_user_id = isset( $_GET['user_id'] ) ? (int) $_GET['user_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + $xml = new Jetpack_IXR_Client(); + $xml->query( 'jetpack.sso.validateResult', $wpcom_nonce, $wpcom_user_id ); + + $user_data = $xml->isError() ? false : $xml->getResponse(); + if ( empty( $user_data ) ) { + add_filter( 'jetpack_sso_default_to_sso_login', '__return_false' ); + add_filter( 'login_message', array( Notices::class, 'error_invalid_response_data' ) ); + return; + } + + $user_data = (object) $user_data; + $user = null; + + /** + * Fires before Jetpack's SSO modifies the log in form. + * + * @module sso + * + * @since jetpack-2.6.0 + * + * @param object $user_data WordPress.com User information. + */ + do_action( 'jetpack_sso_pre_handle_login', $user_data ); + + $tracking = new Tracking(); + + if ( Helpers::is_two_step_required() && 0 === (int) $user_data->two_step_enabled ) { + $this->user_data = $user_data; + + $tracking->record_user_event( + 'sso_login_failed', + array( + 'error_message' => 'error_msg_enable_two_step', + ) + ); + + $error = new WP_Error( 'two_step_required', __( 'You must have Two-Step Authentication enabled on your WordPress.com account.', 'jetpack-connection' ) ); + + /** This filter is documented in core/src/wp-includes/pluggable.php */ + do_action( 'wp_login_failed', $user_data->login, $error ); + add_filter( 'login_message', array( Notices::class, 'error_msg_enable_two_step' ) ); + return; + } + + $user_found_with = ''; + if ( empty( $user ) && isset( $user_data->external_user_id ) ) { + $user_found_with = 'external_user_id'; + $user = get_user_by( 'id', (int) $user_data->external_user_id ); + if ( $user ) { + $expected_id = get_user_meta( $user->ID, 'wpcom_user_id', true ); + if ( $expected_id && $expected_id != $user_data->ID ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison, Universal.Operators.StrictComparisons.LooseNotEqual + $error = new WP_Error( 'expected_wpcom_user', __( 'Something got a little mixed up and an unexpected WordPress.com user logged in.', 'jetpack-connection' ) ); + + $tracking->record_user_event( + 'sso_login_failed', + array( + 'error_message' => 'error_unexpected_wpcom_user', + ) + ); + + /** This filter is documented in core/src/wp-includes/pluggable.php */ + do_action( 'wp_login_failed', $user_data->login, $error ); + add_filter( 'login_message', array( Notices::class, 'error_invalid_response_data' ) ); // @todo Need to have a better notice. This is only for the sake of testing the validation. + return; + } + update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID ); + } + } + + // If we don't have one by wpcom_user_id, try by the email? + if ( empty( $user ) && Helpers::match_by_email() ) { + $user_found_with = 'match_by_email'; + $user = get_user_by( 'email', $user_data->email ); + if ( $user ) { + update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID ); + } + } + + // If we've still got nothing, create the user. + $new_user_override_role = Helpers::new_user_override( $user_data ); + if ( empty( $user ) && ( get_option( 'users_can_register' ) || $new_user_override_role ) ) { + /** + * If not matching by email we still need to verify the email does not exist + * or this blows up + * + * If match_by_email is true, we know the email doesn't exist, as it would have + * been found in the first pass. If get_user_by( 'email' ) doesn't find the + * user, then we know that email is unused, so it's safe to add. + */ + if ( Helpers::match_by_email() || ! get_user_by( 'email', $user_data->email ) ) { + + if ( $new_user_override_role ) { + $user_data->role = $new_user_override_role; + } + + $user = Helpers::generate_user( $user_data ); + if ( ! $user ) { + $tracking->record_user_event( + 'sso_login_failed', + array( + 'error_message' => 'could_not_create_username', + ) + ); + add_filter( 'login_message', array( Notices::class, 'error_unable_to_create_user' ) ); + return; + } + + $user_found_with = $new_user_override_role + ? 'user_created_new_user_override' + : 'user_created_users_can_register'; + } else { + $tracking->record_user_event( + 'sso_login_failed', + array( + 'error_message' => 'error_msg_email_already_exists', + ) + ); + + $this->user_data = $user_data; + add_action( 'login_message', array( Notices::class, 'error_msg_email_already_exists' ) ); + return; + } + } + + /** + * Fires after we got login information from WordPress.com. + * + * @module sso + * + * @since jetpack-2.6.0 + * + * @param WP_User|false|null $user Local User information. + * @param object $user_data WordPress.com User Login information. + */ + do_action( 'jetpack_sso_handle_login', $user, $user_data ); + + if ( $user ) { + // Cache the user's details, so we can present it back to them on their user screen. + update_user_meta( $user->ID, 'wpcom_user_data', $user_data ); + + add_filter( 'auth_cookie_expiration', array( Helpers::class, 'extend_auth_cookie_expiration_for_sso' ) ); + wp_set_auth_cookie( $user->ID, true ); + remove_filter( 'auth_cookie_expiration', array( Helpers::class, 'extend_auth_cookie_expiration_for_sso' ) ); + + /** This filter is documented in core/src/wp-includes/user.php */ + do_action( 'wp_login', $user->user_login, $user ); + + wp_set_current_user( $user->ID ); + + $_request_redirect_to = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $redirect_to = user_can( $user, 'edit_posts' ) ? admin_url() : self::profile_page_url(); + + // If we have a saved redirect to request in a cookie. + if ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) { + // Set that as the requested redirect to. + $redirect_to = esc_url_raw( wp_unslash( $_COOKIE['jetpack_sso_redirect_to'] ) ); + $_request_redirect_to = $redirect_to; + } + + $json_api_auth_environment = Helpers::get_json_api_auth_environment(); + + $is_json_api_auth = ! empty( $json_api_auth_environment ); + $is_user_connected = ( new Manager( 'jetpack-connection' ) )->is_user_connected( $user->ID ); + $roles = new Roles(); + $tracking->record_user_event( + 'sso_user_logged_in', + array( + 'user_found_with' => $user_found_with, + 'user_connected' => (bool) $is_user_connected, + 'user_role' => $roles->translate_current_user_to_role(), + 'is_json_api_auth' => $is_json_api_auth, + ) + ); + + if ( $is_json_api_auth ) { + $jetpack = Jetpack::init(); + $jetpack->verify_json_api_authorization_request( $json_api_auth_environment ); + $jetpack->store_json_api_authorization_token( $user->user_login, $user ); + + } elseif ( ! $is_user_connected ) { + wp_safe_redirect( + add_query_arg( + array( + 'redirect_to' => $redirect_to, + 'request_redirect_to' => $_request_redirect_to, + 'calypso_env' => ( new Host() )->get_calypso_env(), + 'jetpack-sso-auth-redirect' => '1', + ), + admin_url() + ) + ); + exit; + } + + add_filter( 'allowed_redirect_hosts', array( Helpers::class, 'allowed_redirect_hosts' ) ); + wp_safe_redirect( + /** This filter is documented in core/src/wp-login.php */ + apply_filters( 'login_redirect', $redirect_to, $_request_redirect_to, $user ) + ); + exit; + } + + add_filter( 'jetpack_sso_default_to_sso_login', '__return_false' ); + + $tracking->record_user_event( + 'sso_login_failed', + array( + 'error_message' => 'cant_find_user', + ) + ); + + $this->user_data = $user_data; + + $error = new WP_Error( 'account_not_found', __( 'Account not found. If you already have an account, make sure you have connected to WordPress.com.', 'jetpack-connection' ) ); + + /** This filter is documented in core/src/wp-includes/pluggable.php */ + do_action( 'wp_login_failed', $user_data->login, $error ); + add_filter( 'login_message', array( Notices::class, 'cant_find_user' ) ); + } + + /** + * Retrieve the admin profile page URL. + */ + public static function profile_page_url() { + return admin_url( 'profile.php' ); + } + + /** + * Builds the "Login to WordPress.com" button that is displayed on the login page as well as user profile page. + * + * @param array $args An array of arguments to add to the SSO URL. + * @param boolean $is_primary If the button have the `button-primary` class. + * @return string Returns the HTML markup for the button. + */ + public function build_sso_button( $args = array(), $is_primary = false ) { + $url = $this->build_sso_button_url( $args ); + $classes = $is_primary + ? 'jetpack-sso button button-primary' + : 'jetpack-sso button'; + + return sprintf( + '%3$s %4$s', + esc_url( $url ), + $classes, + '', + esc_html__( 'Log in with WordPress.com', 'jetpack-connection' ) + ); + } + + /** + * Builds a URL with `jetpack-sso` action and option args which is used to setup SSO. + * + * @param array $args An array of arguments to add to the SSO URL. + * @return string The URL used for SSO. + */ + public function build_sso_button_url( $args = array() ) { + $defaults = array( + 'action' => 'jetpack-sso', + ); + + $args = wp_parse_args( $args, $defaults ); + + if ( ! empty( $_GET['redirect_to'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $args['redirect_to'] = rawurlencode( esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + + return add_query_arg( $args, wp_login_url() ); + } + + /** + * Retrieves a WordPress.com SSO URL with appropriate query parameters or dies. + * + * @param boolean $reauth If the user be forced to reauthenticate on WordPress.com. + * @param array $args Optional query parameters. + * @return string The WordPress.com SSO URL. + */ + public function get_sso_url_or_die( $reauth = false, $args = array() ) { + $custom_login_url = Helpers::get_custom_login_url(); + if ( $custom_login_url ) { + $args['login_url'] = rawurlencode( $custom_login_url ); + } + + if ( empty( $reauth ) ) { + $sso_redirect = $this->build_sso_url( $args ); + } else { + Helpers::clear_wpcom_profile_cookies(); + $sso_redirect = $this->build_reauth_and_sso_url( $args ); + } + + // If there was an error retrieving the SSO URL, then error. + if ( is_wp_error( $sso_redirect ) ) { + $error_message = sanitize_text_field( + sprintf( '%s: %s', $sso_redirect->get_error_code(), $sso_redirect->get_error_message() ) + ); + $tracking = new Tracking(); + $tracking->record_user_event( + 'sso_login_redirect_failed', + array( + 'error_message' => $error_message, + ) + ); + wp_die( esc_html( $error_message ) ); + } + + return $sso_redirect; + } + + /** + * Build WordPress.com SSO URL with appropriate query parameters. + * + * @param array $args Optional query parameters. + * @return string|WP_Error WordPress.com SSO URL + */ + public function build_sso_url( $args = array() ) { + $sso_nonce = ! empty( $args['sso_nonce'] ) ? $args['sso_nonce'] : self::request_initial_nonce(); + $defaults = array( + 'action' => 'jetpack-sso', + 'site_id' => Manager::get_site_id( true ), + 'sso_nonce' => $sso_nonce, + 'calypso_auth' => '1', + ); + + $args = wp_parse_args( $args, $defaults ); + + if ( is_wp_error( $sso_nonce ) ) { + return $sso_nonce; + } + + return add_query_arg( $args, 'https://wordpress.com/wp-login.php' ); + } + + /** + * Build WordPress.com SSO URL with appropriate query parameters, + * including the parameters necessary to force the user to reauthenticate + * on WordPress.com. + * + * @param array $args Optional query parameters. + * @return string|WP_Error WordPress.com SSO URL + */ + public function build_reauth_and_sso_url( $args = array() ) { + $sso_nonce = ! empty( $args['sso_nonce'] ) ? $args['sso_nonce'] : self::request_initial_nonce(); + $redirect = $this->build_sso_url( + array( + 'force_auth' => '1', + 'sso_nonce' => $sso_nonce, + ) + ); + + if ( is_wp_error( $redirect ) ) { + return $redirect; + } + + $defaults = array( + 'action' => 'jetpack-sso', + 'site_id' => Manager::get_site_id( true ), + 'sso_nonce' => $sso_nonce, + 'reauth' => '1', + 'redirect_to' => rawurlencode( $redirect ), + 'calypso_auth' => '1', + ); + + $args = wp_parse_args( $args, $defaults ); + + if ( is_wp_error( $args['sso_nonce'] ) ) { + return $args['sso_nonce']; + } + + return add_query_arg( $args, 'https://wordpress.com/wp-login.php' ); + } + + /** + * Determines local user associated with a given WordPress.com user ID. + * + * @since jetpack-2.6.0 + * + * @param int $wpcom_user_id User ID from WordPress.com. + * @return null|object Local user object if found, null if not. + */ + public static function get_user_by_wpcom_id( $wpcom_user_id ) { + $user_query = new WP_User_Query( + array( + 'meta_key' => 'wpcom_user_id', + 'meta_value' => (int) $wpcom_user_id, + 'number' => 1, + ) + ); + + $users = $user_query->get_results(); + return $users ? array_shift( $users ) : null; + } + + /** + * When jetpack-sso-auth-redirect query parameter is set, will redirect user to + * WordPress.com authorization flow. + * + * We redirect here instead of in handle_login() because Jetpack::init()->build_connect_url + * calls menu_page_url() which doesn't work properly until admin menus are registered. + */ + public function maybe_authorize_user_after_sso() { + $jetpack = Jetpack::init(); + + if ( empty( $_GET['jetpack-sso-auth-redirect'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return; + } + + $redirect_to = ! empty( $_GET['redirect_to'] ) ? esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) : admin_url(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $request_redirect_to = ! empty( $_GET['request_redirect_to'] ) ? esc_url_raw( wp_unslash( $_GET['request_redirect_to'] ) ) : $redirect_to; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + /** This filter is documented in core/src/wp-login.php */ + $redirect_after_auth = apply_filters( 'login_redirect', $redirect_to, $request_redirect_to, wp_get_current_user() ); + + /** + * Since we are passing this redirect to WordPress.com and therefore can not use wp_safe_redirect(), + * let's sanitize it here to make sure it's safe. If the redirect is not safe, then use admin_url(). + */ + $redirect_after_auth = wp_sanitize_redirect( $redirect_after_auth ); + $redirect_after_auth = wp_validate_redirect( $redirect_after_auth, admin_url() ); + + /** + * Return the raw connect URL with our redirect and attribute connection to SSO. + * We remove any other filters that may be turning on the in-place connection + * since we will be redirecting the user as opposed to iFraming. + */ + remove_all_filters( 'jetpack_use_iframe_authorization_flow' ); + add_filter( 'jetpack_use_iframe_authorization_flow', '__return_false' ); + $connect_url = $jetpack->build_connect_url( true, $redirect_after_auth, 'sso' ); + + add_filter( 'allowed_redirect_hosts', array( Helpers::class, 'allowed_redirect_hosts' ) ); + wp_safe_redirect( $connect_url ); + exit; + } + + /** + * Cache user's display name and Gravatar so it can be displayed on the login screen. These cookies are + * stored when the user logs out, and then deleted when the user logs in. + */ + public function store_wpcom_profile_cookies_on_logout() { + $user_id = get_current_user_id(); + if ( ! ( new Manager( 'jetpack-connection' ) )->is_user_connected( $user_id ) ) { + return; + } + + $user_data = $this->get_user_data( $user_id ); + if ( ! $user_data ) { + return; + } + + setcookie( + 'jetpack_sso_wpcom_name_' . COOKIEHASH, + $user_data->display_name, + time() + WEEK_IN_SECONDS, + COOKIEPATH, + COOKIE_DOMAIN, + is_ssl(), + true + ); + + setcookie( + 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH, + get_avatar_url( + $user_data->email, + array( + 'size' => 144, + 'default' => 'mystery', + ) + ), + time() + WEEK_IN_SECONDS, + COOKIEPATH, + COOKIE_DOMAIN, + is_ssl(), + true + ); + } + + /** + * Determines if a local user is connected to WordPress.com + * + * @since jetpack-2.8 + * @param integer $user_id - Local user id. + * @return boolean + **/ + public function is_user_connected( $user_id ) { + return $this->get_user_data( $user_id ); + } + + /** + * Retrieves a user's WordPress.com data + * + * @since jetpack-2.8 + * @param integer $user_id - Local user id. + * @return mixed null or stdClass + **/ + public function get_user_data( $user_id ) { + return get_user_meta( $user_id, 'wpcom_user_data', true ); + } +} diff --git a/projects/packages/connection/src/sso/class-user-admin.php b/projects/packages/connection/src/sso/class-user-admin.php new file mode 100644 index 0000000000000..c1d0f63b599c9 --- /dev/null +++ b/projects/packages/connection/src/sso/class-user-admin.php @@ -0,0 +1,1296 @@ + 'defer', + 'in_footer' => true, + 'enqueue' => true, + ) + ); + } + + /** + * Intercept the arguments for building the table, and create WP_User_Query instance + * + * @param array $args The search arguments. + * + * @return array + */ + public function set_user_query( $args ) { + self::$user_search = new WP_User_Query( $args ); + return $args; + } + + /** + * Revokes WordPress.com invitation. + * + * @param int $user_id The user ID. + */ + public function revoke_user_invite( $user_id ) { + try { + $has_pending_invite = self::has_pending_wpcom_invite( $user_id ); + + if ( $has_pending_invite ) { + $response = self::send_revoke_wpcom_invite( $has_pending_invite ); + $event = 'sso_user_invite_revoked'; + + if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { + self::$tracking->record_user_event( + $event, + array( + 'success' => 'false', + 'error_message' => 'invalid-revoke-api-error', + ) + ); + return $response; + } + + $body = json_decode( $response['body'] ); + + if ( ! $body->deleted ) { + self::$tracking->record_user_event( + $event, + array( + 'success' => 'false', + 'error_message' => 'invalid-invite-revoke', + ) + ); + } else { + self::$tracking->record_user_event( $event, array( 'success' => 'true' ) ); + } + + return $response; + } else { + // Delete external contributor if it exists. + $wpcom_user_data = ( new Manager() )->get_connected_user_data( $user_id ); + if ( isset( $wpcom_user_data['ID'] ) ) { + return self::delete_external_contributor( $wpcom_user_data['ID'] ); + } + } + } catch ( \Exception $e ) { + return false; + } + } + + /** + * Renders invitations errors/success messages in users.php. + * + * @phan-suppress PhanUndeclaredFunction,UnusedSuppression -- Existence of wp_admin_notice (added in WP 6.4) is checked inline. + * @todo Remove suppression and function_exists check when we drop support for WP 6.3. + */ + public function handle_invitation_results() { + $valid_nonce = isset( $_GET['_wpnonce'] ) + ? wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'jetpack-sso-invite-user' ) + : false; + + if ( ! $valid_nonce || ! isset( $_GET['jetpack-sso-invite-user'] ) || ! function_exists( 'wp_admin_notice' ) ) { + return; + } + if ( $_GET['jetpack-sso-invite-user'] === 'success' ) { + return wp_admin_notice( __( 'User was invited successfully!', 'jetpack-connection' ), array( 'type' => 'success' ) ); + } + if ( $_GET['jetpack-sso-invite-user'] === 'reinvited-success' ) { + return wp_admin_notice( __( 'User was re-invited successfully!', 'jetpack-connection' ), array( 'type' => 'success' ) ); + } + + if ( $_GET['jetpack-sso-invite-user'] === 'successful-revoke' ) { + return wp_admin_notice( __( 'User invite revoked successfully.', 'jetpack-connection' ), array( 'type' => 'success' ) ); + } + + if ( $_GET['jetpack-sso-invite-user'] === 'failed' && isset( $_GET['jetpack-sso-invite-error'] ) ) { + switch ( $_GET['jetpack-sso-invite-error'] ) { + case 'invalid-user': + return wp_admin_notice( __( 'Tried to invite a user that doesn’t exist.', 'jetpack-connection' ), array( 'type' => 'error' ) ); + case 'invalid-email': + return wp_admin_notice( __( 'Tried to invite a user that doesn’t have an email address.', 'jetpack-connection' ), array( 'type' => 'error' ) ); + case 'invalid-user-permissions': + return wp_admin_notice( __( 'You don’t have permission to invite users.', 'jetpack-connection' ), array( 'type' => 'error' ) ); + case 'invalid-user-revoke': + return wp_admin_notice( __( 'Tried to revoke an invite for a user that doesn’t exist.', 'jetpack-connection' ), array( 'type' => 'error' ) ); + case 'invalid-invite-revoke': + return wp_admin_notice( __( 'Tried to revoke an invite that doesn’t exist.', 'jetpack-connection' ), array( 'type' => 'error' ) ); + case 'invalid-revoke-permissions': + return wp_admin_notice( __( 'You don’t have permission to revoke invites.', 'jetpack-connection' ), array( 'type' => 'error' ) ); + case 'empty-invite': + return wp_admin_notice( __( 'There is no previous invite for this user', 'jetpack-connection' ), array( 'type' => 'error' ) ); + case 'invalid-invite': + return wp_admin_notice( __( 'Attempted to send a new invitation to a user using an invite that doesn’t exist.', 'jetpack-connection' ), array( 'type' => 'error' ) ); + case 'error-revoke': + return wp_admin_notice( __( 'An error has occurred when revoking the invite for the user.', 'jetpack-connection' ), array( 'type' => 'error' ) ); + case 'invalid-revoke-api-error': + return wp_admin_notice( __( 'An error has occurred when revoking the user invite.', 'jetpack-connection' ), array( 'type' => 'error' ) ); + default: + return wp_admin_notice( __( 'An error has occurred when inviting the user to the site.', 'jetpack-connection' ), array( 'type' => 'error' ) ); + } + } + } + + /** + * Invites a user to connect to WordPress.com to allow them to log in via SSO. + */ + public function invite_user_to_wpcom() { + check_admin_referer( 'jetpack-sso-invite-user', 'invite_nonce' ); + $nonce = wp_create_nonce( 'jetpack-sso-invite-user' ); + $event = 'sso_user_invite_sent'; + + if ( ! current_user_can( 'create_users' ) ) { + $error = 'invalid-user-permissions'; + $query_params = array( + 'jetpack-sso-invite-user' => 'failed', + 'jetpack-sso-invite-error' => $error, + '_wpnonce' => $nonce, + ); + return self::create_error_notice_and_redirect( $query_params ); + } elseif ( isset( $_GET['user_id'] ) ) { + $user_id = intval( wp_unslash( $_GET['user_id'] ) ); + $user = get_user_by( 'id', $user_id ); + $user_email = $user->user_email; + + if ( ! $user || ! $user_email ) { + $reason = ! $user ? 'invalid-user' : 'invalid-email'; + $query_params = array( + 'jetpack-sso-invite-user' => 'failed', + 'jetpack-sso-invite-error' => $reason, + '_wpnonce' => $nonce, + ); + + self::$tracking->record_user_event( + $event, + array( + 'success' => 'false', + 'error_message' => $reason, + ) + ); + return self::create_error_notice_and_redirect( $query_params ); + } + + $blog_id = Manager::get_site_id( true ); + $roles = new Roles(); + $user_role = $roles->translate_user_to_role( $user ); + + $url = '/sites/' . $blog_id . '/invites/new'; + $response = Client::wpcom_json_api_request_as_user( + $url, + 'v2', + array( + 'method' => 'POST', + ), + array( + 'invitees' => array( + array( + 'email_or_username' => $user_email, + 'role' => $user_role, + ), + ), + ), + 'wpcom' + ); + + if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { + $error = 'invalid-invite-api-error'; + $query_params = array( + 'jetpack-sso-invite-user' => 'failed', + 'jetpack-sso-invite-error' => $error, + '_wpnonce' => $nonce, + ); + + self::$tracking->record_user_event( + $event, + array( + 'success' => 'false', + 'error_message' => $error, + ) + ); + return self::create_error_notice_and_redirect( $query_params ); + } + + $body = json_decode( wp_remote_retrieve_body( $response ) ); + + // access the first item since we're inviting one user. + if ( is_array( $body ) && ! empty( $body ) ) { + $body = $body[0]; + } + + $query_params = array( + 'jetpack-sso-invite-user' => $body->success ? 'success' : 'failed', + '_wpnonce' => $nonce, + ); + + if ( ! $body->success && $body->errors ) { + $response_error = array_keys( (array) $body->errors ); + $query_params['jetpack-sso-invite-error'] = $response_error[0]; + self::$tracking->record_user_event( + $event, + array( + 'success' => 'false', + 'error_message' => $response_error[0], + ) + ); + } else { + self::$tracking->record_user_event( $event, array( 'success' => 'true' ) ); + } + + return self::create_error_notice_and_redirect( $query_params ); + } else { + $error = 'invalid-user'; + $query_params = array( + 'jetpack-sso-invite-user' => 'failed', + 'jetpack-sso-invite-error' => $error, + '_wpnonce' => $nonce, + ); + self::$tracking->record_user_event( + $event, + array( + 'success' => 'false', + 'error_message' => $error, + ) + ); + return self::create_error_notice_and_redirect( $query_params ); + } + wp_die(); + } + + /** + * Revokes a user's invitation to connect to WordPress.com. + * + * @param string $invite_id The ID of the invite to revoke. + */ + public function send_revoke_wpcom_invite( $invite_id ) { + $blog_id = Manager::get_site_id( true ); + + $url = '/sites/' . $blog_id . '/invites/delete'; + return Client::wpcom_json_api_request_as_user( + $url, + 'v2', + array( + 'method' => 'POST', + ), + array( + 'invite_ids' => array( $invite_id ), + ), + 'wpcom' + ); + } + + /** + * Handles logic to revoke user invite. + */ + public function handle_request_revoke_invite() { + check_admin_referer( 'jetpack-sso-revoke-user-invite', 'revoke_invite_nonce' ); + $nonce = wp_create_nonce( 'jetpack-sso-invite-user' ); + $event = 'sso_user_invite_revoked'; + if ( ! current_user_can( 'promote_users' ) ) { + $error = 'invalid-revoke-permissions'; + $query_params = array( + 'jetpack-sso-invite-user' => 'failed', + 'jetpack-sso-invite-error' => $error, + '_wpnonce' => $nonce, + ); + + return self::create_error_notice_and_redirect( $query_params ); + } elseif ( isset( $_GET['user_id'] ) ) { + $user_id = intval( wp_unslash( $_GET['user_id'] ) ); + $user = get_user_by( 'id', $user_id ); + if ( ! $user ) { + $error = 'invalid-user-revoke'; + $query_params = array( + 'jetpack-sso-invite-user' => 'failed', + 'jetpack-sso-invite-error' => $error, + '_wpnonce' => $nonce, + ); + + self::$tracking->record_user_event( + $event, + array( + 'success' => 'false', + 'error_message' => $error, + ) + ); + return self::create_error_notice_and_redirect( $query_params ); + } + + if ( ! isset( $_GET['invite_id'] ) ) { + $error = 'invalid-invite-revoke'; + $query_params = array( + 'jetpack-sso-invite-user' => 'failed', + 'jetpack-sso-invite-error' => $error, + '_wpnonce' => $nonce, + ); + self::$tracking->record_user_event( + $event, + array( + 'success' => 'false', + 'error_message' => $error, + ) + ); + return self::create_error_notice_and_redirect( $query_params ); + } + + $invite_id = sanitize_text_field( wp_unslash( $_GET['invite_id'] ) ); + $response = self::send_revoke_wpcom_invite( $invite_id ); + + if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { + $error = 'invalid-revoke-api-error'; + $query_params = array( + 'jetpack-sso-invite-user' => 'failed', + 'jetpack-sso-invite-error' => $error, // general error message + '_wpnonce' => $nonce, + ); + self::$tracking->record_user_event( + $event, + array( + 'success' => 'false', + 'error_message' => $error, + ) + ); + return self::create_error_notice_and_redirect( $query_params ); + } + + $body = json_decode( $response['body'] ); + $query_params = array( + 'jetpack-sso-invite-user' => $body->deleted ? 'successful-revoke' : 'failed', + '_wpnonce' => $nonce, + ); + if ( ! $body->deleted ) { // no invite was deleted, probably it does not exist + $error = 'invalid-invite-revoke'; + $query_params['jetpack-sso-invite-error'] = $error; + self::$tracking->record_user_event( + $event, + array( + 'success' => 'false', + 'error_message' => $error, + ) + ); + } else { + self::$tracking->record_user_event( $event, array( 'success' => 'true' ) ); + } + return self::create_error_notice_and_redirect( $query_params ); + } else { + $error = 'invalid-user-revoke'; + $query_params = array( + 'jetpack-sso-invite-user' => 'failed', + 'jetpack-sso-invite-error' => $error, + '_wpnonce' => $nonce, + ); + self::$tracking->record_user_event( + $event, + array( + 'success' => 'false', + 'error_message' => $error, + ) + ); + return self::create_error_notice_and_redirect( $query_params ); + } + + wp_die(); + } + + /** + * Handles resend user invite. + */ + public function handle_request_resend_invite() { + check_admin_referer( 'jetpack-sso-resend-user-invite', 'resend_invite_nonce' ); + $nonce = wp_create_nonce( 'jetpack-sso-invite-user' ); + $event = 'sso_user_invite_resend'; + if ( ! current_user_can( 'create_users' ) ) { + $query_params = array( + 'jetpack-sso-invite-user' => 'failed', + 'jetpack-sso-invite-error' => 'invalid-user-permissions', + '_wpnonce' => $nonce, + ); + return self::create_error_notice_and_redirect( $query_params ); + } elseif ( isset( $_GET['invite_id'] ) ) { + $invite_slug = sanitize_text_field( wp_unslash( $_GET['invite_id'] ) ); + $blog_id = Manager::get_site_id( true ); + $url = '/sites/' . $blog_id . '/invites/resend'; + $response = Client::wpcom_json_api_request_as_user( + $url, + 'v2', + array( + 'method' => 'POST', + ), + array( + 'invite_slug' => $invite_slug, + ), + 'wpcom' + ); + + $status_code = wp_remote_retrieve_response_code( $response ); + + if ( 200 !== $status_code ) { + $message_type = $status_code === 404 ? 'invalid-invite' : ''; // empty is the general error message + $query_params = array( + 'jetpack-sso-invite-user' => 'failed', + 'jetpack-sso-invite-error' => $message_type, + '_wpnonce' => $nonce, + ); + self::$tracking->record_user_event( + $event, + array( + 'success' => 'false', + 'error_message' => $message_type, + ) + ); + return self::create_error_notice_and_redirect( $query_params ); + } + + $body = json_decode( $response['body'] ); + $invite_response_message = $body->success ? 'reinvited-success' : 'failed'; + $query_params = array( + 'jetpack-sso-invite-user' => $invite_response_message, + '_wpnonce' => $nonce, + ); + + if ( ! $body->success ) { + self::$tracking->record_user_event( + $event, + array( + 'success' => 'false', + 'error_message' => $invite_response_message, + ) + ); + } else { + self::$tracking->record_user_event( $event, array( 'success' => 'true' ) ); + } + + return self::create_error_notice_and_redirect( $query_params ); + } else { + $error = 'empty-invite'; + $query_params = array( + 'jetpack-sso-invite-user' => 'failed', + 'jetpack-sso-invite-error' => 'empty-invite', + '_wpnonce' => $nonce, + ); + self::$tracking->record_user_event( + $event, + array( + 'success' => 'false', + 'error_message' => $error, + ) + ); + return self::create_error_notice_and_redirect( $query_params ); + } + } + + /** + * Adds 'Revoke invite' and 'Resend invite' link to user table row actions. + * Removes 'Reset password' link. + * + * @param array $actions - User row actions. + * @param WP_User $user_object - User object. + */ + public function jetpack_user_table_row_actions( $actions, $user_object ) { + $user_id = $user_object->ID; + $has_pending_invite = self::has_pending_wpcom_invite( $user_id ); + + if ( current_user_can( 'promote_users' ) && $has_pending_invite ) { + $nonce = wp_create_nonce( 'jetpack-sso-revoke-user-invite' ); + $actions['sso_revoke_invite'] = sprintf( + '%s', + add_query_arg( + array( + 'action' => 'jetpack_revoke_invite_user_to_wpcom', + 'user_id' => $user_id, + 'revoke_invite_nonce' => $nonce, + 'invite_id' => $has_pending_invite, + ), + admin_url( 'admin-post.php' ) + ), + esc_html__( 'Revoke invite', 'jetpack-connection' ) + ); + } + if ( current_user_can( 'promote_users' ) && $has_pending_invite ) { + $nonce = wp_create_nonce( 'jetpack-sso-resend-user-invite' ); + $actions['sso_resend_invite'] = sprintf( + '%s', + add_query_arg( + array( + 'action' => 'jetpack_resend_invite_user_to_wpcom', + 'user_id' => $user_id, + 'resend_invite_nonce' => $nonce, + 'invite_id' => $has_pending_invite, + ), + admin_url( 'admin-post.php' ) + ), + esc_html__( 'Resend invite', 'jetpack-connection' ) + ); + } + + if ( + current_user_can( 'promote_users' ) + && ( + $has_pending_invite + || ( new Manager() )->is_user_connected( $user_id ) + ) + ) { + unset( $actions['resetpassword'] ); + } + + return $actions; + } + + /** + * Render the invitation email message. + */ + public function render_invitation_email_message() { + // @todo Remove function_exists check (and phan suppression below) when we drop support for WP 6.3. + if ( ! function_exists( 'wp_admin_notice' ) ) { + return; + } + $message = wp_kses( + __( + 'We highly recommend inviting users to join WordPress.com and log in securely using Secure Sign On to ensure maximum security and efficiency.', + 'jetpack-connection' + ), + array( + 'a' => array( + 'class' => array(), + 'href' => array(), + 'rel' => array(), + 'target' => array(), + ), + ) + ); + // @phan-suppress-next-line PhanUndeclaredFunction -- Existence of wp_admin_notice (added in WP 6.4) is checked above. @phan-suppress-current-line UnusedPluginSuppression + wp_admin_notice( + $message, + array( + 'id' => 'invitation_message', + 'type' => 'info', + 'dismissible' => false, + 'additional_classes' => array( 'jetpack-sso-admin-create-user-invite-message' ), + ) + ); + } + + /** + * Render a note that wp.com invites will be automatically revoked. + */ + public function render_invitations_notices_for_deleted_users() { + // @todo Remove function_exists check (and phan suppression below) when we drop support for WP 6.3. + if ( ! function_exists( 'wp_admin_notice' ) ) { + return; + } + check_admin_referer( 'bulk-users' ); + + // When one user is deleted, the param is `user`, when multiple users are deleted, the param is `users`. + // We start with `users` and fallback to `user`. + $user_id = isset( $_GET['user'] ) ? intval( wp_unslash( $_GET['user'] ) ) : null; + $user_ids = isset( $_GET['users'] ) ? array_map( 'intval', wp_unslash( $_GET['users'] ) ) : array( $user_id ); + + $users_with_invites = array_filter( + $user_ids, + function ( $user_id ) { + return $user_id !== null && self::has_pending_wpcom_invite( $user_id ); + } + ); + + $users_with_invites = array_map( + function ( $user_id ) { + $user = get_user_by( 'id', $user_id ); + return $user->user_login; + }, + $users_with_invites + ); + + $invites_count = count( $users_with_invites ); + if ( $invites_count > 0 ) { + $users_with_invites = implode( ', ', $users_with_invites ); + $message = wp_kses( + sprintf( + /* translators: %s is a comma-separated list of user logins. */ + _n( + 'WordPress.com invitation will be automatically revoked for user: %s.', + 'WordPress.com invitations will be automatically revoked for users: %s.', + $invites_count, + 'jetpack-connection' + ), + $users_with_invites + ), + array( 'strong' => true ) + ); + // @phan-suppress-next-line PhanUndeclaredFunction -- Existence of wp_admin_notice (added in WP 6.4) is checked above. @phan-suppress-current-line UnusedPluginSuppression + wp_admin_notice( + $message, + array( + 'id' => 'invitation_message', + 'type' => 'info', + 'dismissible' => false, + 'additional_classes' => array( 'jetpack-sso-admin-create-user-invite-message' ), + ) + ); + } + } + + /** + * Render WordPress.com invite checkbox for new user registration. + * + * @param string $type The type of new user form the hook follows. + */ + public function render_wpcom_invite_checkbox( $type ) { + /* + * Only check this box by default on WordPress.com sites + * that do not use the WooCommerce plugin. + */ + $is_checked = ( new Host() )->is_wpcom_platform() && ! class_exists( 'WooCommerce' ); + + if ( $type === 'add-new-user' ) { + ?> + + + + + +
+ + +
+ + + + +
+
+ is_wpcom_platform() ) { + return; + } + + if ( $type === 'add-new-user' ) { + ?> + + + + + +
+ + +
+ + + + +
+
+ + + + + + +
+ + +
+ 500 + ) { + $errors->add( + 'custom_email_message', + wp_kses( + __( 'Error: The custom message is too long. Please keep it under 500 characters.', 'jetpack-connection' ), + array( + 'strong' => array(), + ) + ) + ); + } + + $site_id = Manager::get_site_id( true ); + if ( ! $site_id ) { + $errors->add( + 'invalid_site_id', + wp_kses( + __( 'Error: Invalid site ID.', 'jetpack-connection' ), + array( + 'strong' => array(), + ) + ) + ); + } + + // Bail if there are any errors. + if ( $errors->has_errors() ) { + return $errors; + } + + $new_user_request = array( + 'email_or_username' => sanitize_email( $user->user_email ), + 'role' => sanitize_key( $user->role ), + ); + + if ( + isset( $_POST['custom_email_message'] ) + && strlen( sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) ) ) > 0 + ) { + $new_user_request['message'] = sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) ); + } + + if ( isset( $_POST['user_external_contractor'] ) ) { + $new_user_request['is_external'] = true; + } + + $response = Client::wpcom_json_api_request_as_user( + sprintf( + '/sites/%d/invites/new', + (int) $site_id + ), + '2', // Api version + array( + 'method' => 'POST', + ), + array( + 'invitees' => array( $new_user_request ), + ) + ); + + $event_name = 'sso_new_user_invite_sent'; + $custom_message_sent = isset( $new_user_request['message'] ) ? 'true' : 'false'; + + if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { + $errors->add( + 'invitation_not_sent', + wp_kses( + __( 'Error: The user invitation email could not be sent, the user account was not created.', 'jetpack-connection' ), + array( + 'strong' => array(), + ) + ) + ); + self::$tracking->record_user_event( + $event_name, + array( + 'success' => 'false', + 'error' => wp_remote_retrieve_body( $response ), // Get as much information as possible. + ) + ); + } else { + self::$tracking->record_user_event( + $event_name, + array( + 'success' => 'true', + 'custom_message_sent' => $custom_message_sent, + ) + ); + } + + return $errors; + } + + /** + * Adds a column in the user admin table to display user connection status and actions. + * + * @param array $columns User list table columns. + * + * @return array + */ + public function jetpack_user_connected_th( $columns ) { + Assets::register_script( + 'jetpack-sso-users', + '../../dist/jetpack-sso-users.js', + __FILE__, + array( + 'strategy' => 'defer', + 'in_footer' => true, + 'enqueue' => true, + 'version' => Package_Version::PACKAGE_VERSION, + ) + ); + + $columns['user_jetpack'] = sprintf( + '%2$s [?]%1$s', + esc_attr__( 'Jetpack SSO allows a seamless and secure experience on WordPress.com. Join millions of WordPress users who trust us to keep their accounts safe.', 'jetpack-connection' ), + esc_html__( 'SSO Status', 'jetpack-connection' ), + esc_attr__( 'Tooltip', 'jetpack-connection' ) + ); + return $columns; + } + + /** + * Executed when our WP_User_Query instance is set, and we don't have cached invites. + * This function uses the user emails and the 'are-users-invited' endpoint to build the cache. + * + * @return void + */ + private static function rebuild_invite_cache() { + $blog_id = Manager::get_site_id( true ); + + if ( self::$cached_invites === null && self::$user_search !== null ) { + + self::$cached_invites = array(); + + $results = self::$user_search->get_results(); + + $user_emails = array_reduce( + $results, + function ( $current, $item ) { + if ( ! ( new Manager() )->is_user_connected( $item->ID ) ) { + $current[] = rawurlencode( $item->user_email ); + } else { + self::$cached_invites[] = array( + 'email_or_username' => $item->user_email, + 'invited' => false, + 'invite_code' => '', + ); + } + return $current; + }, + array() + ); + + if ( ! empty( $user_emails ) ) { + $url = '/sites/' . $blog_id . '/invites/are-users-invited'; + + $response = Client::wpcom_json_api_request_as_user( + $url, + 'v2', + array( + 'method' => 'POST', + ), + array( 'users' => $user_emails ), + 'wpcom' + ); + + if ( 200 === wp_remote_retrieve_response_code( $response ) ) { + $body = json_decode( $response['body'], true ); + + // ensure array_merge happens with the right parameters + if ( empty( $body ) ) { + $body = array(); + } + + self::$cached_invites = array_merge( self::$cached_invites, $body ); + } + } + } + } + + /** + * Check if there is cached invite for a user email. + * + * @access private + * @static + * + * @param string $email The user email. + * + * @return array|void Returns the cached invite if found. + */ + public static function get_pending_cached_wpcom_invite( $email ) { + if ( self::$cached_invites === null ) { + self::rebuild_invite_cache(); + } + + if ( ! empty( self::$cached_invites ) && is_array( self::$cached_invites ) ) { + $index = array_search( $email, array_column( self::$cached_invites, 'email_or_username' ), true ); + if ( $index !== false ) { + return self::$cached_invites[ $index ]; + } + } + } + + /** + * Check if a given user is invited to the site. + * + * @access private + * @static + * @param int $user_id The user ID. + * + * @return false|string returns the user invite code if the user is invited, false otherwise. + */ + private static function has_pending_wpcom_invite( $user_id ) { + $blog_id = Manager::get_site_id( true ); + $user = get_user_by( 'id', $user_id ); + $cached_invite = self::get_pending_cached_wpcom_invite( $user->user_email ); + + if ( $cached_invite ) { + return $cached_invite['invite_code']; + } + + $url = '/sites/' . $blog_id . '/invites/is-invited'; + $url = add_query_arg( + array( + 'email_or_username' => rawurlencode( $user->user_email ), + ), + $url + ); + $response = Client::wpcom_json_api_request_as_user( + $url, + 'v2', + array(), + null, + 'wpcom' + ); + + if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { + return false; + } + + $body_response = wp_remote_retrieve_body( $response ); + if ( empty( $body_response ) ) { + return false; + } + + $body = json_decode( $body_response, true ); + if ( ! empty( $body['invite_code'] ) ) { + return $body['invite_code']; + } + + return false; + } + + /** + * Delete an external contributor from the site. + * + * @access private + * @static + * @param int $user_id The user ID. + * + * @return bool Returns true if the user was successfully deleted, false otherwise. + */ + private static function delete_external_contributor( $user_id ) { + $blog_id = Manager::get_site_id( true ); + $url = '/sites/' . $blog_id . '/external-contributors/remove'; + $response = Client::wpcom_json_api_request_as_user( + $url, + 'v2', + array( + 'method' => 'POST', + ), + array( + 'user_id' => $user_id, + ), + 'wpcom' + ); + + if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { + return false; + } + + return true; + } + + /** + * Show Jetpack SSO user connection status. + * + * @param string $val HTML for the column. + * @param string $col User list table column. + * @param int $user_id User ID. + * + * @return string + */ + public function jetpack_show_connection_status( $val, $col, $user_id ) { + if ( 'user_jetpack' === $col ) { + if ( ( new Manager() )->is_user_connected( $user_id ) ) { + $connection_html = sprintf( + '%2$s', + esc_attr__( 'This user is connected and can log-in to this site.', 'jetpack-connection' ), + esc_html__( 'Connected', 'jetpack-connection' ) + ); + return $connection_html; + } else { + $has_pending_invite = self::has_pending_wpcom_invite( $user_id ); + if ( $has_pending_invite ) { + $connection_html = sprintf( + '%2$s', + esc_attr__( 'This user didn’t accept the invitation to join this site yet.', 'jetpack-connection' ), + esc_html__( 'Pending invite', 'jetpack-connection' ) + ); + return $connection_html; + } + $nonce = wp_create_nonce( 'jetpack-sso-invite-user' ); + $connection_html = sprintf( + // Using formmethod and formaction because we can't nest forms and have to submit using the main form. + '%2$s + %3$s + ', + add_query_arg( + array( + 'user_id' => $user_id, + 'invite_nonce' => $nonce, + 'action' => 'jetpack_invite_user_to_wpcom', + ), + admin_url( 'admin-post.php' ) + ), + esc_html__( 'Send invite', 'jetpack-connection' ), + esc_attr__( 'This user doesn’t have an SSO connection to WordPress.com. Invite them to the site to increase security and improve their experience.', 'jetpack-connection' ), + esc_attr__( 'Tooltip', 'jetpack-connection' ) + ); + return $connection_html; + } + } + } + + /** + * Creates error notices and redirects the user to the previous page. + * + * @param array $query_params - query parameters added to redirection URL. + */ + public function create_error_notice_and_redirect( $query_params ) { + $ref = wp_get_referer(); + if ( empty( $ref ) ) { + $ref = network_admin_url( 'users.php' ); + } + + $url = add_query_arg( + $query_params, + $ref + ); + return wp_safe_redirect( $url ); + } + + /** + * Style the Jetpack user rows and columns. + */ + public function jetpack_user_table_styles() { + ?> + + p, +.jetpack-sso-form-display #loginform>div { + display: none; +} + +.jetpack-sso-form-display #loginform #jetpack-sso-wrap { + display: block; +} + +.jetpack-sso-form-display #loginform { + padding: 26px 24px; +} + +.jetpack-sso-or { + margin-bottom: 16px; + position: relative; + text-align: center; +} + +.jetpack-sso-or:before { + background: #dcdcde; + content: ''; + height: 1px; + position: absolute; + left: 0; + top: 50%; + width: 100%; +} + +.jetpack-sso-or span { + background: #fff; + color: #777; + position: relative; + padding: 0 8px; + text-transform: uppercase +} + +#jetpack-sso-wrap .button { + display: flex; + justify-content: center; + align-items: center; + height: 36px; + margin-bottom: 16px; + width: 100%; +} + +#jetpack-sso-wrap .button .genericon-wordpress { + font-size: 24px; + margin-right: 4px; +} + +#jetpack-sso-wrap__user img { + border-radius: 50%; + display: block; + margin: 0 auto 16px; +} + +#jetpack-sso-wrap__user h2 { + font-size: 21px; + font-weight: 300; + margin-bottom: 16px; + text-align: center; +} + +#jetpack-sso-wrap__user h2 span { + font-weight: bold; +} + +.jetpack-sso-wrap__reauth { + margin-bottom: 16px; +} + +.jetpack-sso-form-display #nav { + display: none; +} + +.jetpack-sso-form-display #backtoblog { + margin: 24px 0 0; +} + +.jetpack-sso-clear:after { + content: ""; + display: table; + clear: both; +} diff --git a/projects/packages/connection/src/sso/jetpack-sso-login.js b/projects/packages/connection/src/sso/jetpack-sso-login.js new file mode 100644 index 0000000000000..980741be20c42 --- /dev/null +++ b/projects/packages/connection/src/sso/jetpack-sso-login.js @@ -0,0 +1,27 @@ +document.addEventListener( 'DOMContentLoaded', () => { + const body = document.querySelector( 'body' ), + toggleSSO = document.querySelector( '.jetpack-sso-toggle' ), + userLogin = document.getElementById( 'user_login' ), + userPassword = document.getElementById( 'user_pass' ), + ssoWrap = document.getElementById( 'jetpack-sso-wrap' ), + loginForm = document.getElementById( 'loginform' ), + overflow = document.createElement( 'div' ); + + overflow.className = 'jetpack-sso-clear'; + + loginForm.appendChild( overflow ); + overflow.appendChild( document.querySelector( 'p.forgetmenot' ) ); + overflow.appendChild( document.querySelector( 'p.submit' ) ); + + loginForm.appendChild( ssoWrap ); + body.classList.add( 'jetpack-sso-repositioned' ); + + toggleSSO.addEventListener( 'click', e => { + e.preventDefault(); + body.classList.toggle( 'jetpack-sso-form-display' ); + if ( ! body.classList.contains( 'jetpack-sso-form-display' ) ) { + userLogin.focus(); + userPassword.disabled = false; + } + } ); +} ); diff --git a/projects/packages/connection/src/sso/jetpack-sso-users.js b/projects/packages/connection/src/sso/jetpack-sso-users.js new file mode 100644 index 0000000000000..bf050b5237ba4 --- /dev/null +++ b/projects/packages/connection/src/sso/jetpack-sso-users.js @@ -0,0 +1,12 @@ +document.addEventListener( 'DOMContentLoaded', function () { + document + .querySelectorAll( '.jetpack-sso-invitation-tooltip-icon, #user_jetpack' ) + .forEach( function ( tooltip ) { + tooltip.addEventListener( 'mouseenter', function () { + this.querySelector( '.jetpack-sso-invitation-tooltip' ).style.display = 'block'; + } ); + tooltip.addEventListener( 'mouseleave', function () { + this.querySelector( '.jetpack-sso-invitation-tooltip' ).style.display = 'none'; + } ); + } ); +} ); diff --git a/projects/packages/connection/tests/php/sso/test_Helpers.php b/projects/packages/connection/tests/php/sso/test_Helpers.php new file mode 100644 index 0000000000000..ba395505d673c --- /dev/null +++ b/projects/packages/connection/tests/php/sso/test_Helpers.php @@ -0,0 +1,416 @@ +user_data = (object) array( + 'ID' => 123456789, + 'email' => 'ssouser@testautomattic.com', + 'user_login' => 'ssouser', + 'login' => 'ssouser', + 'display_name' => 'ssouser', + 'first_name' => 'sso', + 'last_name' => 'user', + 'url' => 'https://automattic.com', + 'description' => 'A user to test SSO', + ); + } + + /** + * Clean up the testing environment. + * + * @after + */ + public function tear_down() { + Constants::clear_constants(); + } + + /** + * Return 1. + * + * @return int + */ + public function return_one() { + return 1; + } + + /** + * Test "sso_helpers_is_two_step_required_filter_true". + */ + public function test_sso_helpers_is_two_step_required_filter_true() { + add_filter( 'jetpack_sso_require_two_step', '__return_true' ); + $this->assertTrue( Helpers::is_two_step_required() ); + remove_filter( 'jetpack_sso_require_two_step', '__return_true' ); + } + + /** + * Test "sso_helpers_is_two_step_required_filter_false". + */ + public function test_sso_helpers_is_two_step_required_filter_false() { + add_filter( 'jetpack_sso_require_two_step', '__return_false' ); + $this->assertFalse( Helpers::is_two_step_required() ); + remove_filter( 'jetpack_sso_require_two_step', '__return_false' ); + } + + /** + * Test "sso_helpers_is_two_step_required_option_true". + */ + public function test_sso_helpers_is_two_step_required_option_true() { + update_option( 'jetpack_sso_require_two_step', true ); + $this->assertTrue( Helpers::is_two_step_required() ); + delete_option( 'jetpack_sso_require_two_step' ); + } + + /** + * Test "sso_helpers_is_two_step_required_option_false". + */ + public function test_sso_helpers_is_two_step_required_option_false() { + update_option( 'jetpack_sso_require_two_step', false ); + $this->assertFalse( Helpers::is_two_step_required() ); + delete_option( 'jetpack_sso_require_two_step' ); + } + + /** + * Test "sso_helpers_should_hide_login_form_filter_true". + */ + public function test_sso_helpers_should_hide_login_form_filter_true() { + add_filter( 'jetpack_remove_login_form', '__return_true' ); + $this->assertTrue( Helpers::should_hide_login_form() ); + remove_filter( 'jetpack_remove_login_form', '__return_true' ); + } + + /** + * Test "sso_helpers_should_hide_login_form_filter_false". + */ + public function test_sso_helpers_should_hide_login_form_filter_false() { + add_filter( 'jetpack_remove_login_form', '__return_false' ); + $this->assertFalse( Helpers::should_hide_login_form() ); + remove_filter( 'jetpack_remove_login_form', '__return_false' ); + } + + /** + * Test "sso_helpers_match_by_email_filter_true". + */ + public function test_sso_helpers_match_by_email_filter_true() { + add_filter( 'jetpack_sso_match_by_email', '__return_true' ); + $this->assertTrue( Helpers::match_by_email() ); + remove_filter( 'jetpack_sso_match_by_email', '__return_true' ); + } + + /** + * Test "sso_helpers_match_by_email_filter_false". + */ + public function test_sso_helpers_match_by_email_filter_false() { + add_filter( 'jetpack_sso_match_by_email', '__return_false' ); + $this->assertFalse( Helpers::match_by_email() ); + remove_filter( 'jetpack_sso_match_by_email', '__return_false' ); + } + + /** + * Test "sso_helpers_new_user_override_filter_true_returns_default_role". + */ + public function test_sso_helpers_new_user_override_filter_true_returns_default_role() { + add_role( 'foo', 'Foo' ); + update_option( 'default_role', 'foo' ); + add_filter( 'jetpack_sso_new_user_override', '__return_true' ); + $this->assertEquals( 'foo', Helpers::new_user_override() ); + remove_filter( 'jetpack_sso_new_user_override', '__return_true' ); + } + + /** + * Test "sso_helpers_new_user_override_filter_false". + */ + public function test_sso_helpers_new_user_override_filter_false() { + add_filter( 'jetpack_sso_new_user_override', '__return_false' ); + $this->assertFalse( Helpers::new_user_override() ); + remove_filter( 'jetpack_sso_new_user_override', '__return_false' ); + } + + /** + * Test "sso_helpers_new_user_override_filter_rolename". + */ + public function test_sso_helpers_new_user_override_filter_rolename() { + add_filter( 'jetpack_sso_new_user_override', array( $this, 'return_administrator' ) ); + $this->assertEquals( 'administrator', Helpers::new_user_override() ); + remove_filter( 'jetpack_sso_new_user_override', array( $this, 'return_administrator' ) ); + } + + /** + * Test "sso_helpers_new_user_override_filter_bad_rolename_returns_default". + */ + public function test_sso_helpers_new_user_override_filter_bad_rolename_returns_default() { + add_role( 'foo', 'Foo' ); + update_option( 'default_role', 'foo' ); + add_filter( 'jetpack_sso_new_user_override', array( $this, 'return_foobarbaz' ) ); + $this->assertEquals( 'foo', Helpers::new_user_override() ); + remove_filter( 'jetpack_sso_new_user_override', array( $this, 'return_foobarbaz' ) ); + } + + /** + * Test "sso_helpers_sso_bypass_default_login_form_filter_true". + */ + public function test_sso_helpers_sso_bypass_default_login_form_filter_true() { + add_filter( 'jetpack_sso_bypass_login_forward_wpcom', '__return_true' ); + $this->assertTrue( Helpers::bypass_login_forward_wpcom() ); + remove_filter( 'jetpack_sso_bypass_login_forward_wpcom', '__return_true' ); + } + + /** + * Test "sso_helpers_sso_bypass_default_login_form_filter_false". + */ + public function test_sso_helpers_sso_bypass_default_login_form_filter_false() { + add_filter( 'jetpack_sso_bypass_login_forward_wpcom', '__return_false' ); + $this->assertFalse( Helpers::bypass_login_forward_wpcom() ); + remove_filter( 'jetpack_sso_bypass_login_forward_wpcom', '__return_false' ); + } + + /** + * Test "sso_helpers_require_two_step_disabled". + */ + public function test_sso_helpers_require_two_step_disabled() { + add_filter( 'jetpack_sso_require_two_step', '__return_true' ); + $this->assertTrue( Helpers::is_require_two_step_checkbox_disabled() ); + remove_filter( 'jetpack_sso_require_two_step', '__return_true' ); + } + + /** + * Test "sso_helpers_require_two_step_enabled". + */ + public function test_sso_helpers_require_two_step_enabled() { + $this->assertFalse( Helpers::is_require_two_step_checkbox_disabled() ); + } + + /** + * Test "sso_helpers_match_by_email_disabled". + */ + public function test_sso_helpers_match_by_email_disabled() { + add_filter( 'jetpack_sso_match_by_email', '__return_true' ); + $this->assertTrue( Helpers::is_match_by_email_checkbox_disabled() ); + remove_filter( 'jetpack_sso_match_by_email', '__return_true' ); + } + + /** + * Test "sso_helpers_match_by_email_enabled". + */ + public function test_sso_helpers_match_by_email_enabled() { + $this->assertFalse( Helpers::is_match_by_email_checkbox_disabled() ); + } + + /** + * Test "allow_redirect_hosts_adds_default_hosts". + */ + public function test_allow_redirect_hosts_adds_default_hosts() { + Constants::set_constant( 'JETPACK__API_BASE', 'https://jetpack.wordpress.com/jetpack.' ); + $hosts = Helpers::allowed_redirect_hosts( array( 'test.com' ) ); + $this->assertIsArray( $hosts ); + $this->assertContains( 'test.com', $hosts ); + $this->assertContains( 'wordpress.com', $hosts ); + $this->assertContains( 'jetpack.wordpress.com', $hosts ); + } + + /** + * Test "allowed_redirect_hosts_api_base_added". + */ + public function test_allowed_redirect_hosts_api_base_added() { + $hosts = Helpers::allowed_redirect_hosts( + array( 'test.com' ), + 'http://fakesite.com/jetpack.' + ); + $this->assertIsArray( $hosts ); + $this->assertCount( 6, $hosts ); + $this->assertContains( 'fakesite.com', $hosts ); + } + + /** + * Test "allowed_redirect_hosts_api_base_added_on_dev_version". + */ + public function test_allowed_redirect_hosts_api_base_added_on_dev_version() { + add_filter( 'jetpack_development_version', '__return_true' ); + $hosts = Helpers::allowed_redirect_hosts( + array( 'test.com' ), + 'http://fakesite.com/jetpack.' + ); + $this->assertIsArray( $hosts ); + $this->assertCount( 6, $hosts ); + $this->assertContains( 'fakesite.com', $hosts ); + remove_filter( 'jetpack_development_version', '__return_true' ); + } + + /** + * Test "generate_user_returns_user_when_username_not_exists". + */ + public function test_generate_user_returns_user_when_username_not_exists() { + $user = Helpers::generate_user( $this->user_data ); + $this->assertIsObject( $user ); + $this->assertInstanceOf( 'WP_User', $user ); + + wp_delete_user( $user->ID ); + } + + /** + * Test "generate_user_returns_user_if_username_exists_and_has_tries". + */ + public function test_generate_user_returns_user_if_username_exists_and_has_tries() { + add_filter( 'jetpack_sso_allowed_username_generate_retries', array( $this, 'return_one' ) ); + wp_insert_user( $this->user_data ); + + $user = Helpers::generate_user( $this->user_data ); + + $this->assertIsObject( $user ); + $this->assertInstanceOf( 'WP_User', $user ); + + // If the username contains the user's ID, we know the username was generated with our random algo. + $this->assertStringContainsString( (string) $this->user_data->user_login, $user->user_login ); + + wp_delete_user( $user->ID ); + } + + /** + * Test "generate_user_sets_user_role_when_provided". + */ + public function test_generate_user_sets_user_role_when_provided() { + $this->user_data->role = 'administrator'; + $user = Helpers::generate_user( $this->user_data ); + $this->assertContains( 'administrator', get_userdata( $user->ID )->roles ); + } + + /** + * Test "extend_auth_cookie_casts_to_int". + */ + public function test_extend_auth_cookie_casts_to_int() { + add_filter( 'jetpack_sso_auth_cookie_expiration', array( $this, 'return_string_value' ) ); + $this->assertSame( (int) $this->return_string_value(), Helpers::extend_auth_cookie_expiration_for_sso() ); + remove_filter( 'jetpack_sso_auth_cookie_expiration', array( $this, 'return_string_value' ) ); + } + + /** + * Test "extend_auth_cookie_default_value_greater_than_default". + */ + public function test_extend_auth_cookie_default_value_greater_than_default() { + $this->assertGreaterThan( 2 * DAY_IN_SECONDS, Helpers::extend_auth_cookie_expiration_for_sso() ); + } + + /** + * Test "display_sso_form_for_action". + */ + public function test_display_sso_form_for_action() { + // Let's test the default cases. + $this->assertTrue( Helpers::display_sso_form_for_action( 'login' ) ); + $this->assertTrue( Helpers::display_sso_form_for_action( 'jetpack_json_api_authorization' ) ); + $this->assertFalse( Helpers::display_sso_form_for_action( 'hello_world' ) ); + + add_filter( 'jetpack_sso_allowed_actions', array( $this, 'allow_hello_world_login_action_for_sso' ) ); + $this->assertTrue( Helpers::display_sso_form_for_action( 'hello_world' ) ); + remove_filter( 'jetpack_sso_allowed_actions', array( $this, 'allow_hello_world_login_action_for_sso' ) ); + } + + /** + * Test "get_json_api_auth_environment". + */ + public function test_get_json_api_auth_environment() { + // With no cookie returns false. + $_COOKIE['jetpack_sso_original_request'] = ''; + $this->assertFalse( Helpers::get_json_api_auth_environment() ); + + // With empty query, returns false. + $_COOKIE['jetpack_sso_original_request'] = 'http://website.com'; + $this->assertFalse( Helpers::get_json_api_auth_environment() ); + + // With empty no action query argument, returns false. + $_COOKIE['jetpack_sso_original_request'] = 'http://website.com?hello=world'; + $this->assertFalse( Helpers::get_json_api_auth_environment() ); + + // When action is not for JSON API auth, return false. + $_COOKIE['jetpack_sso_original_request'] = 'http://website.com?action=loggedout'; + $this->assertFalse( Helpers::get_json_api_auth_environment() ); + + // If we pass the other tests, then let's make sure we get the right information back. + $original_request = 'http://website.com/wp-login.php?action=jetpack_json_api_authorization&token=my-token'; + $_COOKIE['jetpack_sso_original_request'] = $original_request; + $environment = Helpers::get_json_api_auth_environment(); + $this->assertIsArray( $environment ); + $this->assertSame( + array( + 'action' => 'jetpack_json_api_authorization', + 'token' => 'my-token', + 'jetpack_json_api_original_query' => $original_request, + ), + $environment + ); + } + + /** + * Test the `get_custom_login_url()` helper. + */ + public function test_get_custom_login_url() { + $login_url_default = Helpers::get_custom_login_url(); + + $custom_url_expected = 'test-login-url/'; + + $custom_url_filter = function ( $login_url ) use ( $custom_url_expected ) { + return str_replace( 'wp-login.php', $custom_url_expected, $login_url ); + }; + add_filter( 'login_url', $custom_url_filter ); + $login_url_custom = Helpers::get_custom_login_url(); + + static::assertNull( $login_url_default ); + static::assertEquals( $custom_url_expected, $login_url_custom ); + } + + /** + * Return string '1'. + * + * @return string + */ + public function return_string_value() { + return '1'; + } + + /** + * Return "administrator". + * + * @return string + */ + public function return_administrator() { + return 'administrator'; + } + + /** + * Return "foobarbaz". + * + * @return string + */ + public function return_foobarbaz() { + return 'foobarbaz'; + } + + /** + * Add "hello_world" action. + * + * @param array $actions Actions. + * @return array + */ + public function allow_hello_world_login_action_for_sso( $actions ) { + $actions[] = 'hello_world'; + return $actions; + } +} diff --git a/projects/packages/connection/tests/php/test_Manager_integration.php b/projects/packages/connection/tests/php/test_Manager_integration.php index ad305fe6067fb..34914ddca5d8d 100644 --- a/projects/packages/connection/tests/php/test_Manager_integration.php +++ b/projects/packages/connection/tests/php/test_Manager_integration.php @@ -26,6 +26,16 @@ class ManagerIntegrationTest extends \WorDBless\BaseTestCase { */ public function set_up() { $this->manager = new Manager(); + Constants::set_constant( 'JETPACK__API_BASE', 'https://jetpack.wordpress.com/jetpack.' ); + } + + /** + * Clean up the testing environment. + * + * @after + */ + public function tear_down() { + Constants::clear_constants(); } /** @@ -539,7 +549,7 @@ public function data_provider_for_test_is_site_connection() { public function test_try_registration() { add_filter( 'pre_http_request', array( Test_REST_Endpoints::class, 'intercept_register_request' ), 10, 3 ); set_transient( 'jetpack_assumed_site_creation_date', '2021-01-01 01:01:01' ); - Constants::$set_constants['JETPACK__API_BASE'] = 'https://jetpack.wordpress.com/jetpack.'; + Constants::set_constant( 'JETPACK__API_BASE', 'https://jetpack.wordpress.com/jetpack.' ); $result = $this->manager->try_registration(); diff --git a/projects/packages/connection/tests/php/test_XMLPC_Async_Call.php b/projects/packages/connection/tests/php/test_XMLPC_Async_Call.php index d584bde60a798..4e3fbb900c6c6 100644 --- a/projects/packages/connection/tests/php/test_XMLPC_Async_Call.php +++ b/projects/packages/connection/tests/php/test_XMLPC_Async_Call.php @@ -7,6 +7,7 @@ namespace Automattic\Jetpack\Connection; +use Automattic\Jetpack\Constants; use WorDBless\BaseTestCase; require_once ABSPATH . WPINC . '/IXR/class-IXR-client.php'; @@ -14,6 +15,21 @@ * Connection Manager functionality testing. */ class XMLRPC_Async_Call_Test extends BaseTestCase { + /** + * Initialize the object before running the test method. + */ + public function set_up() { + Constants::set_constant( 'JETPACK__API_BASE', 'https://jetpack.wordpress.com/jetpack.' ); + } + + /** + * Clean up the testing environment. + * + * @after + */ + public function tear_down() { + Constants::clear_constants(); + } /** * Test add call diff --git a/projects/packages/connection/tests/php/test_jetpack_xmlrpc_server.php b/projects/packages/connection/tests/php/test_jetpack_xmlrpc_server.php index 84c7dce197ff9..8b338d57a2e2e 100644 --- a/projects/packages/connection/tests/php/test_jetpack_xmlrpc_server.php +++ b/projects/packages/connection/tests/php/test_jetpack_xmlrpc_server.php @@ -11,7 +11,6 @@ * Class to test the legacy Jetpack_XMLRPC_Server class. */ class Jetpack_XMLRPC_Server_Test extends BaseTestCase { - use \Yoast\PHPUnitPolyfills\Polyfills\AssertStringContains; /** @@ -36,6 +35,17 @@ protected function set_up() { ( new Tokens() )->update_user_token( $user_id, sprintf( '%s.%s.%d', 'key', 'private', $user_id ), false ); $this->xmlrpc_admin = $user_id; + + Constants::set_constant( 'JETPACK__API_BASE', 'https://jetpack.wordpress.com/jetpack.' ); + } + + /** + * Clean up the testing environment. + * + * @after + */ + public function tear_down() { + Constants::clear_constants(); } /** diff --git a/projects/packages/connection/webpack.config.js b/projects/packages/connection/webpack.config.js index 78f8852197595..2bcc7bcdb9599 100644 --- a/projects/packages/connection/webpack.config.js +++ b/projects/packages/connection/webpack.config.js @@ -1,5 +1,18 @@ const path = require( 'path' ); const jetpackWebpackConfig = require( '@automattic/jetpack-webpack-config/webpack' ); +const glob = require( 'glob' ); + +const ssoEntries = {}; +// Add all js files in the src/sso directory. +for ( const file of glob.sync( './src/sso/*.js' ) ) { + const name = path.basename( file, path.extname( file ) ); + ssoEntries[ name ] = file; +} +// Add all css files as well. +for ( const file of glob.sync( './src/sso/*.css' ) ) { + const name = path.basename( file, path.extname( file ) ); + ssoEntries[ name ] = file; +} module.exports = [ { @@ -12,6 +25,7 @@ module.exports = [ type: 'window', }, }, + ...ssoEntries, }, mode: jetpackWebpackConfig.mode, devtool: jetpackWebpackConfig.devtool, @@ -26,12 +40,21 @@ module.exports = [ ...jetpackWebpackConfig.resolve, }, node: false, - plugins: [ ...jetpackWebpackConfig.StandardPlugins() ], + plugins: [ + ...jetpackWebpackConfig.StandardPlugins( { + MiniCssExtractPlugin: { filename: '[name].css' }, + } ), + ], module: { strictExportPresence: true, rules: [ // Transpile JavaScript, including node_modules. jetpackWebpackConfig.TranspileRule(), + + // Handle CSS. + jetpackWebpackConfig.CssRule( { + extensions: [ 'css' ], + } ), ], }, }, diff --git a/projects/packages/forms/.phan/baseline.php b/projects/packages/forms/.phan/baseline.php index d6ae9df9ddb4c..dfa59f7e7a9a8 100644 --- a/projects/packages/forms/.phan/baseline.php +++ b/projects/packages/forms/.phan/baseline.php @@ -15,8 +15,8 @@ // PhanTypeMismatchReturnProbablyReal : 10+ occurrences // PhanUndeclaredFunction : 9 occurrences // PhanTypeMismatchArgumentInternal : 7 occurrences - // PhanTypeMismatchArgumentProbablyReal : 7 occurrences // PhanUndeclaredClassProperty : 7 occurrences + // PhanTypeMismatchArgumentProbablyReal : 6 occurrences // PhanDeprecatedFunction : 5 occurrences // PhanUndeclaredMethod : 5 occurrences // PhanRedundantCondition : 4 occurrences @@ -54,7 +54,7 @@ 'src/contact-form/class-contact-form.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanPluginRedundantAssignment', 'PhanRedundantCondition', 'PhanTypeMismatchArgument', 'PhanTypeMismatchArgumentNullableInternal', 'PhanTypeMismatchProperty', 'PhanTypeMismatchReturnNullable', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredClassMethod', 'PhanUndeclaredClassProperty', 'PhanUndeclaredTypeParameter', 'PhanUndeclaredTypeProperty', 'PhanUnextractableAnnotationElementName'], 'src/dashboard/class-dashboard-view-switch.php' => ['PhanUnreferencedUseNormal'], 'src/dashboard/class-dashboard.php' => ['PhanUndeclaredClassMethod', 'PhanUndeclaredConstant'], - 'src/service/class-google-drive.php' => ['PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredClassMethod', 'PhanUndeclaredFunction'], + 'src/service/class-google-drive.php' => ['PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredClassMethod', 'PhanUndeclaredFunction'], 'tests/php/contact-form/test-class.contact-form-plugin.php' => ['PhanPluginMixedKeyNoKey'], 'tests/php/contact-form/test-class.contact-form.php' => ['PhanDeprecatedFunction', 'PhanTypeMismatchArgument', 'PhanUndeclaredClassMethod', 'PhanUndeclaredClassProperty', 'PhanUndeclaredMethod', 'PhanUndeclaredTypeParameter', 'PhanUndeclaredTypeReturnType'], ], diff --git a/projects/packages/forms/changelog/add-sso-classes-connection b/projects/packages/forms/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..d88cd1b29112c --- /dev/null +++ b/projects/packages/forms/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Phan: update baseline files + + diff --git a/projects/packages/my-jetpack/.phan/baseline.php b/projects/packages/my-jetpack/.phan/baseline.php index ffe44e9e63787..f98551ac6186a 100644 --- a/projects/packages/my-jetpack/.phan/baseline.php +++ b/projects/packages/my-jetpack/.phan/baseline.php @@ -12,9 +12,9 @@ // PhanTypeMismatchArgumentNullable : 60+ occurrences // PhanTypeMismatchPropertyDefault : 15+ occurrences // PhanParamTooMany : 10+ occurrences - // PhanTypeMismatchArgumentProbablyReal : 10+ occurrences // PhanTypeMismatchReturnProbablyReal : 10+ occurrences // PhanAbstractStaticMethodCallInStatic : 8 occurrences + // PhanTypeMismatchArgumentProbablyReal : 8 occurrences // PhanUndeclaredClassMethod : 8 occurrences // PhanNoopNew : 6 occurrences // PhanTypeMismatchReturn : 6 occurrences @@ -43,10 +43,10 @@ 'src/class-initializer.php' => ['PhanImpossibleCondition', 'PhanNoopNew', 'PhanParamTooMany', 'PhanRedundantCondition', 'PhanTypeMismatchArgumentInternal', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnNullable', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredClassMethod', 'PhanUndeclaredClassProperty', 'PhanUndeclaredTypeProperty'], 'src/class-jetpack-manage.php' => ['PhanTypeMismatchArgumentProbablyReal'], 'src/class-products.php' => ['PhanNonClassMethodCall'], - 'src/class-rest-product-data.php' => ['PhanParamTooMany', 'PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchReturn'], + 'src/class-rest-product-data.php' => ['PhanParamTooMany', 'PhanTypeMismatchReturn'], 'src/class-rest-products.php' => ['PhanParamTooMany', 'PhanPluginMixedKeyNoKey', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal'], 'src/class-rest-purchases.php' => ['PhanParamTooMany', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal'], - 'src/class-rest-zendesk-chat.php' => ['PhanParamTooMany', 'PhanTypeMismatchArgumentProbablyReal', 'PhanUndeclaredFunction', 'PhanUnextractableAnnotationSuffix'], + 'src/class-rest-zendesk-chat.php' => ['PhanParamTooMany', 'PhanUndeclaredFunction', 'PhanUnextractableAnnotationSuffix'], 'src/class-wpcom-products.php' => ['PhanTypeMismatchReturnProbablyReal', 'PhanUnextractableAnnotation'], 'src/products/class-anti-spam.php' => ['PhanTypeMismatchArgumentNullable', 'PhanTypeMismatchPropertyDefault'], 'src/products/class-backup.php' => ['PhanTypeMismatchArgumentNullable', 'PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchPropertyDefault'], diff --git a/projects/packages/my-jetpack/changelog/add-sso-classes-connection b/projects/packages/my-jetpack/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..d88cd1b29112c --- /dev/null +++ b/projects/packages/my-jetpack/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Phan: update baseline files + + diff --git a/projects/packages/my-jetpack/package.json b/projects/packages/my-jetpack/package.json index 2de85de302d42..ee0f3cbda4531 100644 --- a/projects/packages/my-jetpack/package.json +++ b/projects/packages/my-jetpack/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@automattic/jetpack-my-jetpack", - "version": "4.22.0", + "version": "4.22.1-alpha", "description": "WP Admin page with information and configuration shared among all Jetpack stand-alone plugins", "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/packages/my-jetpack/#readme", "bugs": { diff --git a/projects/packages/my-jetpack/src/class-initializer.php b/projects/packages/my-jetpack/src/class-initializer.php index 3465c9114e3af..dec5ebbe2f278 100644 --- a/projects/packages/my-jetpack/src/class-initializer.php +++ b/projects/packages/my-jetpack/src/class-initializer.php @@ -37,7 +37,7 @@ class Initializer { * * @var string */ - const PACKAGE_VERSION = '4.22.0'; + const PACKAGE_VERSION = '4.22.1-alpha'; /** * HTML container ID for the IDC screen on My Jetpack page. diff --git a/projects/packages/plans/.phan/baseline.php b/projects/packages/plans/.phan/baseline.php index e40937ae693db..735a0d10c0b99 100644 --- a/projects/packages/plans/.phan/baseline.php +++ b/projects/packages/plans/.phan/baseline.php @@ -11,7 +11,6 @@ // # Issue statistics: // PhanUndeclaredFunction : 2 occurrences // PhanPluginMixedKeyNoKey : 1 occurrence - // PhanTypeMismatchArgumentProbablyReal : 1 occurrence // PhanTypeMismatchPropertyProbablyReal : 1 occurrence // PhanTypeMismatchReturn : 1 occurrence // PhanTypeMismatchReturnProbablyReal : 1 occurrence @@ -20,7 +19,7 @@ // Currently, file_suppressions and directory_suppressions are the only supported suppressions 'file_suppressions' => [ 'src/class-current-plan.php' => ['PhanTypeMismatchPropertyProbablyReal', 'PhanUndeclaredFunction'], - 'src/class-plans.php' => ['PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredClassMethod'], + 'src/class-plans.php' => ['PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredClassMethod'], 'tests/php/test-current-plan.php' => ['PhanPluginMixedKeyNoKey'], ], // 'directory_suppressions' => ['src/directory_name' => ['PhanIssueName1', 'PhanIssueName2']] can be manually added if needed. diff --git a/projects/packages/plans/changelog/add-sso-classes-connection b/projects/packages/plans/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..57a02313ce197 --- /dev/null +++ b/projects/packages/plans/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Phan: configuration updates. + + diff --git a/projects/packages/plans/package.json b/projects/packages/plans/package.json index 9ec75e63e95fd..1c7127bef313a 100644 --- a/projects/packages/plans/package.json +++ b/projects/packages/plans/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@automattic/jetpack-plans", - "version": "0.4.4", + "version": "0.4.5-alpha", "description": "Fetch information about Jetpack Plans from wpcom", "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/packages/plans/#readme", "bugs": { diff --git a/projects/packages/publicize/.phan/baseline.php b/projects/packages/publicize/.phan/baseline.php index 6417b16a590ed..c35189c2ac172 100644 --- a/projects/packages/publicize/.phan/baseline.php +++ b/projects/packages/publicize/.phan/baseline.php @@ -11,7 +11,7 @@ // # Issue statistics: // PhanUndeclaredClassMethod : 20+ occurrences // PhanTypeMismatchArgument : 15+ occurrences - // PhanTypeMismatchArgumentProbablyReal : 15+ occurrences + // PhanTypeMismatchArgumentProbablyReal : 10+ occurrences // PhanDeprecatedFunction : 9 occurrences // PhanPluginDuplicateConditionalNullCoalescing : 7 occurrences // PhanTypeMismatchProperty : 7 occurrences @@ -45,7 +45,7 @@ 'src/class-publicize-setup.php' => ['PhanTypeMismatchArgument', 'PhanUnextractableAnnotationSuffix'], 'src/class-publicize-ui.php' => ['PhanPluginDuplicateExpressionAssignmentOperation', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredClassMethod'], 'src/class-publicize.php' => ['PhanParamSignatureMismatch', 'PhanPossiblyUndeclaredVariable', 'PhanTypeMismatchArgument', 'PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMissingReturn', 'PhanUndeclaredClassMethod', 'PhanUndeclaredFunction', 'PhanUndeclaredStaticMethod'], - 'src/class-rest-controller.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredMethod'], + 'src/class-rest-controller.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredMethod'], 'src/social-image-generator/class-post-settings.php' => ['PhanPluginDuplicateConditionalNullCoalescing'], 'src/social-image-generator/class-rest-settings-controller.php' => ['PhanPluginMixedKeyNoKey'], 'src/social-image-generator/class-settings.php' => ['PhanPluginDuplicateConditionalNullCoalescing'], diff --git a/projects/packages/publicize/changelog/add-sso-classes-connection b/projects/packages/publicize/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..d88cd1b29112c --- /dev/null +++ b/projects/packages/publicize/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Phan: update baseline files + + diff --git a/projects/packages/publicize/package.json b/projects/packages/publicize/package.json index 62c4bb4650932..f909e006eaf50 100644 --- a/projects/packages/publicize/package.json +++ b/projects/packages/publicize/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@automattic/jetpack-publicize", - "version": "0.42.10", + "version": "0.42.11-alpha", "description": "Publicize makes it easy to share your site’s posts on several social media networks automatically when you publish a new post.", "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/packages/publicize/#readme", "bugs": { diff --git a/projects/packages/stats-admin/changelog/add-sso-classes-connection b/projects/packages/stats-admin/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..d88cd1b29112c --- /dev/null +++ b/projects/packages/stats-admin/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Phan: update baseline files + + diff --git a/projects/packages/stats-admin/package.json b/projects/packages/stats-admin/package.json index 9e1ad4d427d4e..9472d08083f3b 100644 --- a/projects/packages/stats-admin/package.json +++ b/projects/packages/stats-admin/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@automattic/jetpack-stats-admin", - "version": "0.18.1", + "version": "0.18.2-alpha", "description": "Stats Dashboard", "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/packages/stats-admin/#readme", "bugs": { diff --git a/projects/packages/stats-admin/src/class-main.php b/projects/packages/stats-admin/src/class-main.php index 4bac40c9c88fb..a8c1e5ff1e118 100644 --- a/projects/packages/stats-admin/src/class-main.php +++ b/projects/packages/stats-admin/src/class-main.php @@ -22,7 +22,7 @@ class Main { /** * Stats version. */ - const VERSION = '0.18.1'; + const VERSION = '0.18.2-alpha'; /** * Singleton Main instance. diff --git a/projects/plugins/automattic-for-agencies-client/changelog/add-sso-classes-connection b/projects/plugins/automattic-for-agencies-client/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..9aa70e3ec1f75 --- /dev/null +++ b/projects/plugins/automattic-for-agencies-client/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Updated composer.lock. + + diff --git a/projects/plugins/automattic-for-agencies-client/composer.lock b/projects/plugins/automattic-for-agencies-client/composer.lock index bdcf8c4a08ac4..37ceb1005d49f 100644 --- a/projects/plugins/automattic-for-agencies-client/composer.lock +++ b/projects/plugins/automattic-for-agencies-client/composer.lock @@ -353,11 +353,12 @@ "dist": { "type": "path", "url": "../../packages/connection", - "reference": "c8993518f67538ea9d7f898077aa597e4cdc0970" + "reference": "4b7d9b428cd74c5b33129e0712e3dd417b8268bf" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", "automattic/jetpack-admin-ui": "@dev", + "automattic/jetpack-assets": "@dev", "automattic/jetpack-constants": "@dev", "automattic/jetpack-redirect": "@dev", "automattic/jetpack-roles": "@dev", diff --git a/projects/plugins/backup/changelog/add-sso-classes-connection b/projects/plugins/backup/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..9aa70e3ec1f75 --- /dev/null +++ b/projects/plugins/backup/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Updated composer.lock. + + diff --git a/projects/plugins/backup/composer.lock b/projects/plugins/backup/composer.lock index 577db9e9e4585..edd2d743acdf5 100644 --- a/projects/plugins/backup/composer.lock +++ b/projects/plugins/backup/composer.lock @@ -634,11 +634,12 @@ "dist": { "type": "path", "url": "../../packages/connection", - "reference": "c8993518f67538ea9d7f898077aa597e4cdc0970" + "reference": "4b7d9b428cd74c5b33129e0712e3dd417b8268bf" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", "automattic/jetpack-admin-ui": "@dev", + "automattic/jetpack-assets": "@dev", "automattic/jetpack-constants": "@dev", "automattic/jetpack-redirect": "@dev", "automattic/jetpack-roles": "@dev", diff --git a/projects/plugins/boost/changelog/add-sso-classes-connection b/projects/plugins/boost/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..9aa70e3ec1f75 --- /dev/null +++ b/projects/plugins/boost/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Updated composer.lock. + + diff --git a/projects/plugins/boost/composer.lock b/projects/plugins/boost/composer.lock index 25148d5700a7e..3a7e8e6e5dc44 100644 --- a/projects/plugins/boost/composer.lock +++ b/projects/plugins/boost/composer.lock @@ -490,11 +490,12 @@ "dist": { "type": "path", "url": "../../packages/connection", - "reference": "c8993518f67538ea9d7f898077aa597e4cdc0970" + "reference": "4b7d9b428cd74c5b33129e0712e3dd417b8268bf" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", "automattic/jetpack-admin-ui": "@dev", + "automattic/jetpack-assets": "@dev", "automattic/jetpack-constants": "@dev", "automattic/jetpack-redirect": "@dev", "automattic/jetpack-roles": "@dev", diff --git a/projects/plugins/inspect/changelog/add-sso-classes-connection b/projects/plugins/inspect/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..9aa70e3ec1f75 --- /dev/null +++ b/projects/plugins/inspect/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Updated composer.lock. + + diff --git a/projects/plugins/inspect/composer.lock b/projects/plugins/inspect/composer.lock index 487605b4b8c1d..633e0b86d3f77 100644 --- a/projects/plugins/inspect/composer.lock +++ b/projects/plugins/inspect/composer.lock @@ -353,11 +353,12 @@ "dist": { "type": "path", "url": "../../packages/connection", - "reference": "c8993518f67538ea9d7f898077aa597e4cdc0970" + "reference": "4b7d9b428cd74c5b33129e0712e3dd417b8268bf" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", "automattic/jetpack-admin-ui": "@dev", + "automattic/jetpack-assets": "@dev", "automattic/jetpack-constants": "@dev", "automattic/jetpack-redirect": "@dev", "automattic/jetpack-roles": "@dev", diff --git a/projects/plugins/jetpack/.phan/baseline.php b/projects/plugins/jetpack/.phan/baseline.php index 90d84e5d0eaa0..edd4cc8abfb6a 100644 --- a/projects/plugins/jetpack/.phan/baseline.php +++ b/projects/plugins/jetpack/.phan/baseline.php @@ -11,7 +11,7 @@ // # Issue statistics: // PhanTypeMismatchArgument : 560+ occurrences // PhanUndeclaredMethod : 370+ occurrences - // PhanTypeMismatchArgumentProbablyReal : 360+ occurrences + // PhanTypeMismatchArgumentProbablyReal : 350+ occurrences // PhanPluginDuplicateConditionalNullCoalescing : 290+ occurrences // PhanUndeclaredClassMethod : 240+ occurrences // PhanNoopNew : 210+ occurrences @@ -137,16 +137,16 @@ '3rd-party/qtranslate-x.php' => ['PhanTypeMismatchReturn'], '3rd-party/woocommerce.php' => ['PhanParamTooMany'], '3rd-party/wpml.php' => ['PhanUndeclaredFunction'], - '_inc/blogging-prompts.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanTypeArraySuspicious', 'PhanTypeMismatchArgumentInternal', 'PhanTypeMismatchArgumentProbablyReal', 'PhanUndeclaredClassMethod'], + '_inc/blogging-prompts.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanTypeArraySuspicious', 'PhanTypeMismatchArgumentInternal', 'PhanUndeclaredClassMethod'], '_inc/class.jetpack-provision.php' => ['PhanAccessMethodInternal', 'PhanTypeMismatchArgument', 'PhanTypeMismatchReturnNullable'], '_inc/genericons.php' => ['PhanTypeMismatchArgumentProbablyReal'], '_inc/jetpack-server-sandbox.php' => ['PhanSuspiciousMagicConstant', 'PhanUndeclaredClassMethod'], '_inc/lib/admin-pages/class-jetpack-about-page.php' => ['PhanTypeMismatchArgument', 'PhanTypeMismatchArgumentProbablyReal'], '_inc/lib/admin-pages/class-jetpack-redux-state-helper.php' => ['PhanParamTooMany', 'PhanPluginDuplicateConditionalNullCoalescing', 'PhanRedundantCondition', 'PhanTypeMismatchArgument', 'PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchDimAssignment'], '_inc/lib/admin-pages/class.jetpack-admin-page.php' => ['PhanDeprecatedProperty', 'PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredProperty'], - '_inc/lib/class-jetpack-ai-helper.php' => ['PhanTypeMismatchArgument', 'PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchPropertyDefault', 'PhanUndeclaredClassMethod', 'PhanUndeclaredFunction'], + '_inc/lib/class-jetpack-ai-helper.php' => ['PhanTypeMismatchArgument', 'PhanTypeMismatchPropertyDefault', 'PhanUndeclaredClassMethod', 'PhanUndeclaredFunction'], '_inc/lib/class-jetpack-currencies.php' => ['PhanTypeMismatchArgument', 'PhanTypeMismatchArgumentInternal'], - '_inc/lib/class-jetpack-google-drive-helper.php' => ['PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredClassMethod', 'PhanUndeclaredFunction'], + '_inc/lib/class-jetpack-google-drive-helper.php' => ['PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredClassMethod', 'PhanUndeclaredFunction'], '_inc/lib/class-jetpack-instagram-gallery-helper.php' => ['PhanTypeMismatchArgument', 'PhanTypeMismatchArgumentProbablyReal', 'PhanUndeclaredClassMethod', 'PhanUndeclaredFunction'], '_inc/lib/class-jetpack-mapbox-helper.php' => ['PhanTypeMismatchArgumentNullable', 'PhanUndeclaredFunction'], '_inc/lib/class-jetpack-podcast-feed-locator.php' => ['PhanDeprecatedFunctionInternal'], @@ -175,14 +175,14 @@ '_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-newsletter-categories-list.php' => ['PhanUndeclaredFunction', 'PhanUndeclaredMethod'], '_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-newsletter-categories-subscriptions-count.php' => ['PhanUndeclaredFunction'], '_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-podcast-player.php' => ['PhanTypeMismatchReturn'], - '_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-publicize-share-post.php' => ['PhanTypeMismatchArgumentProbablyReal', 'PhanUndeclaredClassMethod'], + '_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-publicize-share-post.php' => ['PhanUndeclaredClassMethod'], '_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-related-posts.php' => ['PhanPluginMixedKeyNoKey', 'PhanTypeMismatchReturn'], '_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-resolve-redirect.php' => ['PhanPluginMixedKeyNoKey', 'PhanUndeclaredClassProperty', 'PhanUndeclaredTypeParameter'], '_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-send-email-preview.php' => ['PhanTypeMismatchReturn', 'PhanUndeclaredClassMethod'], '_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-template-loader.php' => ['PhanTypeMismatchReturnProbablyReal'], '_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v3-endpoint-blogging-prompts.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanPluginMixedKeyNoKey', 'PhanTypeInvalidLeftOperandOfAdd', 'PhanTypeMismatchArgumentInternal', 'PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredClassMethod', 'PhanUndeclaredFunction'], '_inc/lib/core-api/wpcom-endpoints/gutenberg-available-extensions.php' => ['PhanPluginMixedKeyNoKey'], - '_inc/lib/core-api/wpcom-endpoints/memberships.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanPluginUnreachableCode', 'PhanTypeArraySuspicious', 'PhanTypeArraySuspiciousNullable', 'PhanTypeMismatchArgument', 'PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal', 'PhanTypeSuspiciousStringExpression', 'PhanUndeclaredClassConstant', 'PhanUndeclaredClassInstanceof', 'PhanUndeclaredClassMethod', 'PhanUndeclaredFunction'], + '_inc/lib/core-api/wpcom-endpoints/memberships.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanPluginUnreachableCode', 'PhanTypeArraySuspicious', 'PhanTypeArraySuspiciousNullable', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal', 'PhanTypeSuspiciousStringExpression', 'PhanUndeclaredClassConstant', 'PhanUndeclaredClassInstanceof', 'PhanUndeclaredClassMethod', 'PhanUndeclaredFunction'], '_inc/lib/core-api/wpcom-endpoints/publicize-connection-test-results.php' => ['PhanPluginMixedKeyNoKey', 'PhanTypeMismatchArgument', 'PhanUndeclaredClassMethod'], '_inc/lib/core-api/wpcom-endpoints/publicize-connections.php' => ['PhanParamSignatureMismatch', 'PhanPluginMixedKeyNoKey', 'PhanTypeMismatchArgument', 'PhanUndeclaredClassMethod'], '_inc/lib/core-api/wpcom-endpoints/publicize-services.php' => ['PhanParamSignatureMismatch', 'PhanPluginMixedKeyNoKey', 'PhanTypeMismatchArgument', 'PhanUndeclaredClassMethod'], @@ -197,7 +197,7 @@ '_inc/lib/functions.wp-notify.php' => ['PhanTypeMismatchReturn'], '_inc/lib/icalendar-reader.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanPluginDuplicateExpressionAssignmentOperation', 'PhanPossiblyUndeclaredVariable', 'PhanRedundantCondition', 'PhanTypeInvalidLeftOperandOfNumericOp', 'PhanTypeMismatchArgumentNullableInternal', 'PhanTypeMismatchDimFetch', 'PhanTypeMismatchReturnProbablyReal', 'PhanTypePossiblyInvalidDimOffset', 'PhanUndeclaredProperty'], '_inc/lib/markdown/extra.php' => ['PhanImpossibleConditionInLoop', 'PhanPossiblyUndeclaredVariable', 'PhanTypeArraySuspiciousNullable', 'PhanTypeMismatchArgumentInternal', 'PhanTypeMismatchArgumentNullableInternal', 'PhanTypeMismatchReturn', 'PhanUndeclaredProperty', 'PhanUndeclaredVariableDim'], - '_inc/lib/plans.php' => ['PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredClassMethod'], + '_inc/lib/plans.php' => ['PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredClassMethod'], '_inc/lib/tonesque.php' => ['PhanTypeMismatchArgument', 'PhanTypeMismatchArgumentInternal', 'PhanTypeMismatchArgumentProbablyReal'], '_inc/lib/widgets.php' => ['PhanRedundantConditionInLoop', 'PhanTypeArraySuspiciousNullable', 'PhanTypeMismatchArgumentNullable', 'PhanTypeMismatchReturn', 'PhanUnextractableAnnotationElementName'], '_inc/social-logos.php' => ['PhanTypeMismatchArgumentProbablyReal'], @@ -495,7 +495,7 @@ 'modules/sso.php' => ['PhanNoopNew', 'PhanRedundantCondition', 'PhanTypeMismatchArgument', 'PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal'], 'modules/sso/class-jetpack-force-2fa.php' => ['PhanDeprecatedFunction'], 'modules/sso/class.jetpack-sso-helpers.php' => ['PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchReturn'], - 'modules/sso/class.jetpack-sso-user-admin.php' => ['PhanPluginUnreachableCode', 'PhanTypeArraySuspiciousNullable', 'PhanTypeMismatchArgument', 'PhanTypeMismatchArgumentInternal', 'PhanTypeMismatchArgumentProbablyReal', 'PhanUnextractableAnnotation'], + 'modules/sso/class.jetpack-sso-user-admin.php' => ['PhanPluginUnreachableCode', 'PhanTypeArraySuspiciousNullable', 'PhanTypeMismatchArgument', 'PhanTypeMismatchArgumentInternal', 'PhanUnextractableAnnotation'], 'modules/stats.php' => ['PhanDeprecatedFunction', 'PhanPossiblyUndeclaredVariable', 'PhanRedundantCondition', 'PhanSuspiciousMagicConstant', 'PhanTypeMismatchArgument', 'PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnNullable', 'PhanTypeMismatchReturnProbablyReal', 'PhanTypeMissingReturn', 'PhanUnextractableAnnotationSuffix'], 'modules/subscriptions.php' => ['PhanPossiblyUndeclaredVariable', 'PhanTypeMismatchArgument', 'PhanTypeMismatchArgumentInternal', 'PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchDefault', 'PhanTypeMismatchReturnProbablyReal', 'PhanTypeSuspiciousNonTraversableForeach', 'PhanUndeclaredFunctionInCallable'], 'modules/subscriptions/subscribe-modal/class-jetpack-subscribe-modal.php' => ['PhanTypeMismatchReturnNullable'], @@ -582,7 +582,6 @@ 'modules/wordads/php/class-wordads-sidebar-widget.php' => ['PhanTypeExpectedObjectPropAccessButGotNull', 'PhanTypeMismatchArgument'], 'modules/wpcom-block-editor/class-jetpack-wpcom-block-editor.php' => ['PhanPluginUseReturnValueInternalKnown'], 'modules/wpcom-block-editor/functions.editor-type.php' => ['PhanUndeclaredFunction'], - 'modules/wpcom-tos/wpcom-tos.php' => ['PhanTypeMismatchArgumentProbablyReal'], 'sal/class.json-api-date.php' => ['PhanPluginDuplicateExpressionAssignmentOperation', 'PhanRedundantCondition', 'PhanTypeMismatchArgumentInternalProbablyReal'], 'sal/class.json-api-links.php' => ['PhanTypeMismatchArgumentInternal', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal'], 'sal/class.json-api-post-base.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanRedundantCondition', 'PhanTypeMismatchArgument', 'PhanTypeMismatchReturnNullable', 'PhanTypeMismatchReturnProbablyReal', 'PhanTypePossiblyInvalidDimOffset', 'PhanTypeSuspiciousNonTraversableForeach', 'PhanUndeclaredFunction'], diff --git a/projects/plugins/jetpack/changelog/add-sso-classes-connection b/projects/plugins/jetpack/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..a1c1831fa1ef7 --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: other +Comment: Updated composer.lock. + + diff --git a/projects/plugins/jetpack/composer.lock b/projects/plugins/jetpack/composer.lock index b841ab51ea5d9..23bb791725cfa 100644 --- a/projects/plugins/jetpack/composer.lock +++ b/projects/plugins/jetpack/composer.lock @@ -867,11 +867,12 @@ "dist": { "type": "path", "url": "../../packages/connection", - "reference": "c8993518f67538ea9d7f898077aa597e4cdc0970" + "reference": "4b7d9b428cd74c5b33129e0712e3dd417b8268bf" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", "automattic/jetpack-admin-ui": "@dev", + "automattic/jetpack-assets": "@dev", "automattic/jetpack-constants": "@dev", "automattic/jetpack-redirect": "@dev", "automattic/jetpack-roles": "@dev", diff --git a/projects/plugins/migration/.phan/baseline.php b/projects/plugins/migration/.phan/baseline.php index 9b9f72c40a007..0cc480d0c6556 100644 --- a/projects/plugins/migration/.phan/baseline.php +++ b/projects/plugins/migration/.phan/baseline.php @@ -11,11 +11,10 @@ // # Issue statistics: // PhanNoopNew : 2 occurrences // PhanPluginDuplicateConditionalNullCoalescing : 1 occurrence - // PhanTypeMismatchArgumentProbablyReal : 1 occurrence // Currently, file_suppressions and directory_suppressions are the only supported suppressions 'file_suppressions' => [ - 'src/class-rest-controller.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanTypeMismatchArgumentProbablyReal'], + 'src/class-rest-controller.php' => ['PhanPluginDuplicateConditionalNullCoalescing'], 'src/class-wpcom-migration.php' => ['PhanNoopNew'], 'wpcom-migration.php' => ['PhanNoopNew'], ], diff --git a/projects/plugins/migration/changelog/add-sso-classes-connection b/projects/plugins/migration/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..9aa70e3ec1f75 --- /dev/null +++ b/projects/plugins/migration/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Updated composer.lock. + + diff --git a/projects/plugins/migration/composer.lock b/projects/plugins/migration/composer.lock index 700f4d5a4dff4..0945d737bc9d0 100644 --- a/projects/plugins/migration/composer.lock +++ b/projects/plugins/migration/composer.lock @@ -634,11 +634,12 @@ "dist": { "type": "path", "url": "../../packages/connection", - "reference": "c8993518f67538ea9d7f898077aa597e4cdc0970" + "reference": "4b7d9b428cd74c5b33129e0712e3dd417b8268bf" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", "automattic/jetpack-admin-ui": "@dev", + "automattic/jetpack-assets": "@dev", "automattic/jetpack-constants": "@dev", "automattic/jetpack-redirect": "@dev", "automattic/jetpack-roles": "@dev", diff --git a/projects/plugins/protect/.phan/baseline.php b/projects/plugins/protect/.phan/baseline.php index c56696a52cdb1..90fdeda614490 100644 --- a/projects/plugins/protect/.phan/baseline.php +++ b/projects/plugins/protect/.phan/baseline.php @@ -12,7 +12,7 @@ // PhanPluginDuplicateConditionalNullCoalescing : 45+ occurrences // PhanParamTooMany : 6 occurrences // PhanTypeMismatchArgument : 5 occurrences - // PhanTypeMismatchArgumentProbablyReal : 5 occurrences + // PhanTypeMismatchArgumentProbablyReal : 4 occurrences // PhanTypeMismatchProperty : 2 occurrences // PhanTypeMismatchReturnProbablyReal : 2 occurrences // PhanNoopNew : 1 occurrence diff --git a/projects/plugins/protect/changelog/add-sso-classes-connection b/projects/plugins/protect/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..9aa70e3ec1f75 --- /dev/null +++ b/projects/plugins/protect/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Updated composer.lock. + + diff --git a/projects/plugins/protect/composer.lock b/projects/plugins/protect/composer.lock index 273483697ab1f..b9da5b268cab5 100644 --- a/projects/plugins/protect/composer.lock +++ b/projects/plugins/protect/composer.lock @@ -547,11 +547,12 @@ "dist": { "type": "path", "url": "../../packages/connection", - "reference": "c8993518f67538ea9d7f898077aa597e4cdc0970" + "reference": "4b7d9b428cd74c5b33129e0712e3dd417b8268bf" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", "automattic/jetpack-admin-ui": "@dev", + "automattic/jetpack-assets": "@dev", "automattic/jetpack-constants": "@dev", "automattic/jetpack-redirect": "@dev", "automattic/jetpack-roles": "@dev", diff --git a/projects/plugins/search/changelog/add-sso-classes-connection b/projects/plugins/search/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..9aa70e3ec1f75 --- /dev/null +++ b/projects/plugins/search/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Updated composer.lock. + + diff --git a/projects/plugins/search/composer.lock b/projects/plugins/search/composer.lock index 53a9f40bbcf05..4216da00234dc 100644 --- a/projects/plugins/search/composer.lock +++ b/projects/plugins/search/composer.lock @@ -490,11 +490,12 @@ "dist": { "type": "path", "url": "../../packages/connection", - "reference": "c8993518f67538ea9d7f898077aa597e4cdc0970" + "reference": "4b7d9b428cd74c5b33129e0712e3dd417b8268bf" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", "automattic/jetpack-admin-ui": "@dev", + "automattic/jetpack-assets": "@dev", "automattic/jetpack-constants": "@dev", "automattic/jetpack-redirect": "@dev", "automattic/jetpack-roles": "@dev", diff --git a/projects/plugins/social/changelog/add-sso-classes-connection b/projects/plugins/social/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..9aa70e3ec1f75 --- /dev/null +++ b/projects/plugins/social/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Updated composer.lock. + + diff --git a/projects/plugins/social/composer.lock b/projects/plugins/social/composer.lock index b44dad7f71b92..eb6e72423f72d 100644 --- a/projects/plugins/social/composer.lock +++ b/projects/plugins/social/composer.lock @@ -490,11 +490,12 @@ "dist": { "type": "path", "url": "../../packages/connection", - "reference": "c8993518f67538ea9d7f898077aa597e4cdc0970" + "reference": "4b7d9b428cd74c5b33129e0712e3dd417b8268bf" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", "automattic/jetpack-admin-ui": "@dev", + "automattic/jetpack-assets": "@dev", "automattic/jetpack-constants": "@dev", "automattic/jetpack-redirect": "@dev", "automattic/jetpack-roles": "@dev", diff --git a/projects/plugins/starter-plugin/changelog/add-sso-classes-connection b/projects/plugins/starter-plugin/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..9aa70e3ec1f75 --- /dev/null +++ b/projects/plugins/starter-plugin/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Updated composer.lock. + + diff --git a/projects/plugins/starter-plugin/composer.lock b/projects/plugins/starter-plugin/composer.lock index 6e6de1467f28e..d81a4300e40a6 100644 --- a/projects/plugins/starter-plugin/composer.lock +++ b/projects/plugins/starter-plugin/composer.lock @@ -490,11 +490,12 @@ "dist": { "type": "path", "url": "../../packages/connection", - "reference": "c8993518f67538ea9d7f898077aa597e4cdc0970" + "reference": "4b7d9b428cd74c5b33129e0712e3dd417b8268bf" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", "automattic/jetpack-admin-ui": "@dev", + "automattic/jetpack-assets": "@dev", "automattic/jetpack-constants": "@dev", "automattic/jetpack-redirect": "@dev", "automattic/jetpack-roles": "@dev", diff --git a/projects/plugins/videopress/changelog/add-sso-classes-connection b/projects/plugins/videopress/changelog/add-sso-classes-connection new file mode 100644 index 0000000000000..9aa70e3ec1f75 --- /dev/null +++ b/projects/plugins/videopress/changelog/add-sso-classes-connection @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Updated composer.lock. + + diff --git a/projects/plugins/videopress/composer.lock b/projects/plugins/videopress/composer.lock index e16c6b4437fc7..770733625bf8c 100644 --- a/projects/plugins/videopress/composer.lock +++ b/projects/plugins/videopress/composer.lock @@ -490,11 +490,12 @@ "dist": { "type": "path", "url": "../../packages/connection", - "reference": "c8993518f67538ea9d7f898077aa597e4cdc0970" + "reference": "4b7d9b428cd74c5b33129e0712e3dd417b8268bf" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", "automattic/jetpack-admin-ui": "@dev", + "automattic/jetpack-assets": "@dev", "automattic/jetpack-constants": "@dev", "automattic/jetpack-redirect": "@dev", "automattic/jetpack-roles": "@dev",