diff --git a/projects/packages/publicize/.phan/baseline.php b/projects/packages/publicize/.phan/baseline.php index 276871afd3219..e6d2be7e491bc 100644 --- a/projects/packages/publicize/.phan/baseline.php +++ b/projects/packages/publicize/.phan/baseline.php @@ -32,11 +32,13 @@ 'file_suppressions' => [ 'src/class-connections-post-field.php' => ['PhanPluginDuplicateConditionalNullCoalescing'], 'src/class-keyring-helper.php' => ['PhanTypeMismatchArgumentProbablyReal', 'PhanTypeMismatchDefault'], + 'src/class-publicize-assets.php' => ['PhanNoopNew'], 'src/class-publicize-base.php' => ['PhanImpossibleCondition', 'PhanPluginDuplicateConditionalNullCoalescing', 'PhanPluginSimplifyExpressionBool', 'PhanSuspiciousMagicConstant', 'PhanTypeMismatchArgument', 'PhanTypeMismatchArgumentNullable', 'PhanTypeMismatchArgumentNullableInternal', 'PhanTypeMismatchDimFetch', 'PhanTypeMismatchReturn'], 'src/class-publicize-setup.php' => ['PhanTypeMismatchArgument'], 'src/class-publicize-ui.php' => ['PhanPluginDuplicateExpressionAssignmentOperation', 'PhanTypeMismatchReturnProbablyReal'], 'src/class-publicize.php' => ['PhanParamSignatureMismatch', 'PhanPossiblyUndeclaredVariable', 'PhanTypeMismatchArgument', 'PhanTypeMissingReturn'], 'src/class-rest-controller.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanTypeMismatchReturnProbablyReal'], + 'src/rest-endpoints/class-connections-controller.php' => ['PhanPluginMixedKeyNoKey'], '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/.phan/config.php b/projects/packages/publicize/.phan/config.php index 7030e2aa8176c..075dd16643b9e 100644 --- a/projects/packages/publicize/.phan/config.php +++ b/projects/packages/publicize/.phan/config.php @@ -24,6 +24,7 @@ __DIR__ . '/../../../plugins/jetpack/_inc/lib/admin-pages/class.jetpack-admin-page.php', // class Jetpack_Admin_Page __DIR__ . '/../../../plugins/jetpack/modules/subscriptions.php', // class Jetpack_Subscriptions __DIR__ . '/../../../plugins/jetpack/functions.global.php', // function jetpack_render_tos_blurb + __DIR__ . '/../../../plugins/jetpack/_inc/lib/core-api/load-wpcom-endpoints.php', // function wpcom_rest_api_v2_load_plugin ), ) ); diff --git a/projects/packages/publicize/changelog/add-social-unitied-connections-endpoint b/projects/packages/publicize/changelog/add-social-unitied-connections-endpoint new file mode 100644 index 0000000000000..5f31aa8db8050 --- /dev/null +++ b/projects/packages/publicize/changelog/add-social-unitied-connections-endpoint @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Added wpcom/v3/publicize/connections endpoint diff --git a/projects/packages/publicize/src/class-connection-fields.php b/projects/packages/publicize/src/class-connection-fields.php new file mode 100644 index 0000000000000..a9de08bc176a3 --- /dev/null +++ b/projects/packages/publicize/src/class-connection-fields.php @@ -0,0 +1,170 @@ +get_connection_meta( $connection ); + } + + /** + * Get the ID of a connection. + * + * @param array $connection The connection. + * @return string + */ + public static function get_connection_id( $connection ) { + return (string) self::publicize()->get_connection_id( $connection ); + } + + /** + * Returns a display name for the Connection + * + * @param string $service_name 'facebook', 'twitter', etc. + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return string + */ + public static function get_display_name( $service_name, $connection ) { + return self::publicize()->get_display_name( $service_name, $connection ); + } + + /** + * Returns the external handle for the Connection. + * + * @param string $service_name 'facebook', 'linkedin', etc. + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return string + */ + public static function get_external_handle( $service_name, $connection ) { + $cmeta = self::get_connection_meta( $connection ); + + switch ( $service_name ) { + case 'mastodon': + return $cmeta['external_display'] ?? ''; + + case 'bluesky': + case 'threads': + return $cmeta['external_name'] ?? ''; + + case 'instagram-business': + return $cmeta['connection_data']['meta']['username'] ?? ''; + + default: + return ''; + } + } + + /** + * Returns the external ID for the Connection. + * + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return string + */ + public static function get_external_id( $connection ) { + $connection_meta = self::get_connection_meta( $connection ); + + return $connection_meta['external_id'] ?? ''; + } + + /** + * Returns an external URL to the Connection's profile + * + * @param string $service_name 'facebook', 'twitter', etc. + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return false|string False on failure. URL on success. + */ + public static function get_profile_link( $service_name, $connection ) { + return self::publicize()->get_profile_link( $service_name, $connection ); + } + + /** + * Returns a profile picture for the Connection + * + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return string + */ + public static function get_profile_picture( $connection ) { + return self::publicize()->get_profile_picture( $connection ); + } + + /** + * Returns a display name for the Service + * + * @param string $service_name 'facebook', 'twitter', etc. + * @return string + */ + public static function get_service_label( $service_name ) { + return self::publicize()->get_service_label( $service_name ); + } + + /** + * Returns whether the Connection is shared + * + * @param array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return bool + */ + public static function is_shared( $connection ) { + return empty( self::get_user_id( $connection ) ); + } + + /** + * Returns the status for the Connection + * + * @param array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return string + */ + public static function get_status( $connection ) { + return $connection['status'] ?? 'ok'; + } + + /** + * Returns the user ID for the Connection + * + * @param array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return int + */ + public static function get_user_id( $connection ) { + $connection_meta = self::get_connection_meta( $connection ); + + $connection_data = $connection_meta['connection_data']; + + return (int) $connection_data['user_id']; + } +} diff --git a/projects/packages/publicize/src/class-publicize-assets.php b/projects/packages/publicize/src/class-publicize-assets.php index e74fe44dcf5bc..f3e718ae6b9f3 100644 --- a/projects/packages/publicize/src/class-publicize-assets.php +++ b/projects/packages/publicize/src/class-publicize-assets.php @@ -7,6 +7,8 @@ namespace Automattic\Jetpack\Publicize; +use Automattic\Jetpack\Publicize\Rest_Endpoints\Connections_Controller; + /** * Publicize_Assets class. */ @@ -17,5 +19,6 @@ class Publicize_Assets { */ public static function configure() { Publicize_Script_Data::configure(); + new Connections_Controller(); } } diff --git a/projects/packages/publicize/src/class-publicize-base.php b/projects/packages/publicize/src/class-publicize-base.php index b88bd1a6b12ca..8494b8e1e9280 100644 --- a/projects/packages/publicize/src/class-publicize-base.php +++ b/projects/packages/publicize/src/class-publicize-base.php @@ -497,8 +497,8 @@ public function get_profile_link( $service_name, $connection ) { return 'https://instagram.com/' . $cmeta['connection_data']['meta']['username']; } - if ( 'threads' === $service_name && isset( $connection['external_name'] ) ) { - return 'https://www.threads.net/@' . $connection['external_name']; + if ( 'threads' === $service_name && isset( $cmeta['external_name'] ) ) { + return 'https://www.threads.net/@' . $cmeta['external_name']; } if ( 'mastodon' === $service_name && isset( $cmeta['external_name'] ) ) { @@ -527,7 +527,7 @@ public function get_profile_link( $service_name, $connection ) { } $profile_url_query = wp_parse_url( $cmeta['connection_data']['meta']['profile_url'], PHP_URL_QUERY ); - $profile_url_query_args = null; + $profile_url_query_args = array(); wp_parse_str( $profile_url_query, $profile_url_query_args ); $id = null; @@ -608,7 +608,7 @@ public function get_username( $service_name, $connection ) { * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack). * @return string */ - private function get_profile_picture( $connection ) { + public function get_profile_picture( $connection ) { $cmeta = $this->get_connection_meta( $connection ); if ( isset( $cmeta['profile_picture'] ) ) { diff --git a/projects/packages/publicize/src/rest-endpoints/class-base-controller.php b/projects/packages/publicize/src/rest-endpoints/class-base-controller.php new file mode 100644 index 0000000000000..5dd19761093de --- /dev/null +++ b/projects/packages/publicize/src/rest-endpoints/class-base-controller.php @@ -0,0 +1,90 @@ +namespace = 'wpcom/v3'; + + $this->wpcom_is_wpcom_only_endpoint = true; + } + + /** + * Check if we are on WPCOM. + * + * @return bool + */ + public static function is_wpcom() { + return ( new Host() )->is_wpcom_simple(); + } + + /** + * Filters out data based on ?_fields= request parameter + * + * @param array $item Item to prepare. + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_REST_Response filtered item + */ + public function prepare_item_for_response( $item, $request ) { + if ( ! is_callable( array( $this, 'get_fields_for_response' ) ) ) { + return rest_ensure_response( $item ); + } + + $fields = $this->get_fields_for_response( $request ); + + $response_data = array(); + foreach ( $item as $field => $value ) { + if ( in_array( $field, $fields, true ) ) { + $response_data[ $field ] = $value; + } + } + + return rest_ensure_response( $response_data ); + } + + /** + * Verify that user can access Publicize data + * + * @return true|WP_Error + */ + public function get_items_permission_check() { + global $publicize; + + if ( ! $publicize ) { + return new WP_Error( + 'publicize_not_available', + __( 'Sorry, Jetpack Social is not available on your site right now.', 'jetpack-publicize-pkg' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + if ( $publicize->current_user_can_access_publicize_data() ) { + return true; + } + + return new WP_Error( + 'invalid_user_permission_publicize', + __( 'Sorry, you are not allowed to access Jetpack Social data on this site.', 'jetpack-publicize-pkg' ), + array( 'status' => rest_authorization_required_code() ) + ); + } +} diff --git a/projects/packages/publicize/src/rest-endpoints/class-connections-controller.php b/projects/packages/publicize/src/rest-endpoints/class-connections-controller.php new file mode 100644 index 0000000000000..2c5e4973ca6f7 --- /dev/null +++ b/projects/packages/publicize/src/rest-endpoints/class-connections-controller.php @@ -0,0 +1,254 @@ +rest_base = 'publicize/connections'; + + add_action( 'rest_api_init', array( $this, 'register_routes' ) ); + } + + /** + * Register the routes. + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permission_check' ), + 'args' => array( + 'test_connections' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to test connections.', 'jetpack-publicize-pkg' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Schema for the endpoint. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'jetpack-publicize-connection', + 'type' => 'object', + 'properties' => array( + 'connection_id' => array( + 'type' => 'string', + 'description' => __( 'Connection ID of the connected account.', 'jetpack-publicize-pkg' ), + ), + 'display_name' => array( + 'type' => 'string', + 'description' => __( 'Display name of the connected account.', 'jetpack-publicize-pkg' ), + ), + 'external_handle' => array( + 'type' => 'string', + 'description' => __( 'The external handle or username of the connected account.', 'jetpack-publicize-pkg' ), + ), + 'external_id' => array( + 'type' => 'string', + 'description' => __( 'The external ID of the connected account.', 'jetpack-publicize-pkg' ), + ), + 'profile_link' => array( + 'type' => 'string', + 'description' => __( 'Profile link of the connected account.', 'jetpack-publicize-pkg' ), + ), + 'profile_picture' => array( + 'type' => 'string', + 'description' => __( 'URL of the profile picture of the connected account.', 'jetpack-publicize-pkg' ), + ), + 'service_label' => array( + 'type' => 'string', + 'description' => __( 'Human-readable label for the Jetpack Social service.', 'jetpack-publicize-pkg' ), + ), + 'service_name' => array( + 'type' => 'string', + 'description' => __( 'Alphanumeric identifier for the Jetpack Social service.', 'jetpack-publicize-pkg' ), + ), + 'shared' => array( + 'type' => 'boolean', + 'description' => __( 'Whether the connection is shared with other users.', 'jetpack-publicize-pkg' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'The connection status.', 'jetpack-publicize-pkg' ), + 'enum' => array( + 'ok', + 'broken', + ), + ), + 'user_id' => array( + 'type' => 'integer', + 'description' => __( 'ID of the user the connection belongs to.', 'jetpack-publicize-pkg' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get all connections. Meant to be called directly only on WPCOM. + * + * @param bool $run_tests Whether to run tests on the connections. + * + * @return array + */ + protected static function get_all_connections( $run_tests = false ) { + /** + * Publicize instance. + * + * @var \Automattic\Jetpack\Publicize\Publicize $publicize + */ + global $publicize; + + $items = array(); + + $test_results = $run_tests ? self::get_connections_test_status() : array(); + + foreach ( (array) $publicize->get_services( 'connected' ) as $service_name => $connections ) { + foreach ( $connections as $connection ) { + + $connection_id = Connection_Fields::get_connection_id( $connection ); + + $items[] = array( + 'connection_id' => $connection_id, + 'display_name' => Connection_Fields::get_display_name( $service_name, $connection ), + 'external_handle' => Connection_Fields::get_external_handle( $service_name, $connection ), + 'external_id' => Connection_Fields::get_external_id( $connection ), + 'profile_link' => Connection_Fields::get_profile_link( $service_name, $connection ), + 'profile_picture' => Connection_Fields::get_profile_picture( $connection ), + 'service_label' => Connection_Fields::get_service_label( $service_name ), + 'service_name' => $service_name, + 'shared' => Connection_Fields::is_shared( $connection ), + 'status' => $test_results[ $connection_id ] ?? 'ok', + 'user_id' => Connection_Fields::get_user_id( $connection ), + ); + } + } + + return $items; + } + + /** + * Get a list of publicize connections. + * + * @param bool $run_tests Whether to run tests on the connections. + * + * @return array + */ + public static function get_connections( $run_tests = false ) { + if ( self::is_wpcom() ) { + return self::get_all_connections( $run_tests ); + } + + $site_id = Manager::get_site_id( true ); + if ( ! $site_id ) { + return array(); + } + + $path = add_query_arg( + array( 'test_connections' => $run_tests ), + sprintf( '/sites/%d/publicize/connections', $site_id ) + ); + + $response = Client::wpcom_json_api_request_as_user( $path, 'v3', array( 'method' => 'GET' ) ); + + if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { + // TODO log error. + return array(); + } + + $body = wp_remote_retrieve_body( $response ); + + $items = json_decode( $body, true ); + + return $items ? $items : array(); + } + + /** + * Get list of connected Publicize connections. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_REST_Response suitable for 1-page collection + */ + public function get_items( $request ) { + $items = array(); + + $run_tests = $request->get_param( 'test_connections' ); + + foreach ( self::get_connections( $run_tests ) as $item ) { + $data = $this->prepare_item_for_response( $item, $request ); + + $items[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $items ); + $response->header( 'X-WP-Total', (string) count( $items ) ); + $response->header( 'X-WP-TotalPages', '1' ); + + return $response; + } + + /** + * Get the connections test status. + * + * @return array + */ + protected static function get_connections_test_status() { + /** + * Publicize instance. + * + * @var \Automattic\Jetpack\Publicize\Publicize $publicize + */ + global $publicize; + + $test_results = $publicize->get_publicize_conns_test_results(); + + $test_results_map = array(); + + foreach ( $test_results as $test_result ) { + // Compare to `true` because the API returns a 'must_reauth' for LinkedIn. + $test_results_map[ $test_result['connectionID'] ] = true === $test_result['connectionTestPassed'] ? 'ok' : 'broken'; + } + + return $test_results_map; + } +} + +if ( Base_Controller::is_wpcom() ) { + wpcom_rest_api_v2_load_plugin( Connections_Controller::class ); +}