This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,182 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.34] - 2026-06-29
### Changed
- Internal updates.
## [0.1.33] - 2026-06-25
### Changed
- Update package dependencies. [#49831]
## [0.1.32] - 2026-06-22
### Changed
- Update package dependencies. [#49631] [#49691] [#49757]
## [0.1.31] - 2026-06-09
### Changed
- Update package dependencies. [#49273]
## [0.1.30] - 2026-06-08
### Changed
- Update dependencies. [#49354]
## [0.1.29] - 2026-05-25
### Changed
- Update package dependencies. [#48405]
## [0.1.28] - 2026-05-19
### Changed
- Exclude development files from production builds. [#47365]
- Update dependencies. [#48778]
## [0.1.27] - 2026-05-11
### Changed
- Update dependencies. [#46383]
## [0.1.26] - 2026-05-04
### Changed
- Internal: No longer require automattic/jetpack-changelogger as a per-project dev dependency. [#48225]
## [0.1.25] - 2026-04-20
### Changed
- Update package dependencies. [#48106]
## [0.1.24] - 2026-04-13
### Changed
- Update package dependencies. [#47890]
## [0.1.23] - 2026-04-06
### Changed
- Update dependencies. [#46383]
## [0.1.22] - 2026-03-30
### Changed
- Update dependencies. [#46383]
## [0.1.21] - 2026-03-23
### Changed
- Update package dependencies. [#47684]
## [0.1.20] - 2026-03-16
### Changed
- Update dependencies. [#47472]
## [0.1.19] - 2026-03-09
### Changed
- Update package dependencies. [#47496]
## [0.1.18] - 2026-02-26
### Changed
- Update package dependencies. [#47300]
## [0.1.17] - 2026-02-23
### Changed
- Update package dependencies. [#47173]
## [0.1.16] - 2026-02-16
### Changed
- Update package dependencies. [#47099]
## [0.1.15] - 2026-02-10
### Changed
- Update package dependencies. [#47002]
## [0.1.14] - 2026-02-02
### Changed
- Update package dependencies. [#46854]
## [0.1.13] - 2026-01-26
### Changed
- Update dependencies. [#46383]
## [0.1.12] - 2026-01-19
### Changed
- Update package dependencies. [#46552] [#46647]
## [0.1.11] - 2026-01-12
### Changed
- Update package dependencies. [#46456]
## [0.1.10] - 2025-12-22
### Changed
- Update dependencies. [#46381]
## [0.1.9] - 2025-12-15
### Changed
- Internal updates.
## [0.1.8] - 2025-12-08
### Fixed
- Ensure proper flags are used with `json_encode()`. [#46092]
## [0.1.7] - 2025-12-01
### Changed
- Update package dependencies. [#46143]
## [0.1.6] - 2025-11-17
### Changed
- Update package dependencies. [#45915]
## [0.1.5] - 2025-11-03
### Changed
- Update dependencies. [#45664]
## [0.1.4] - 2025-10-06
### Changed
- Update package dependencies. [#45334]
## [0.1.3] - 2025-09-22
### Changed
- Update dependencies.
## [0.1.2] - 2025-09-19
### Changed
- Update package dependencies. [#45173] [#45229]
## [0.1.1] - 2025-09-08
### Changed
- Update package dependencies. [#45027]
## 0.1.0 - 2025-09-01
### Added
- Initial version. [#44858]
[0.1.34]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.33...v0.1.34
[0.1.33]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.32...v0.1.33
[0.1.32]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.31...v0.1.32
[0.1.31]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.30...v0.1.31
[0.1.30]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.29...v0.1.30
[0.1.29]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.28...v0.1.29
[0.1.28]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.27...v0.1.28
[0.1.27]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.26...v0.1.27
[0.1.26]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.25...v0.1.26
[0.1.25]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.24...v0.1.25
[0.1.24]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.23...v0.1.24
[0.1.23]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.22...v0.1.23
[0.1.22]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.21...v0.1.22
[0.1.21]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.20...v0.1.21
[0.1.20]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.19...v0.1.20
[0.1.19]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.18...v0.1.19
[0.1.18]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.17...v0.1.18
[0.1.17]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.16...v0.1.17
[0.1.16]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.15...v0.1.16
[0.1.15]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.14...v0.1.15
[0.1.14]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.13...v0.1.14
[0.1.13]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.12...v0.1.13
[0.1.12]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.11...v0.1.12
[0.1.11]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.10...v0.1.11
[0.1.10]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.9...v0.1.10
[0.1.9]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.8...v0.1.9
[0.1.8]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.7...v0.1.8
[0.1.7]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.6...v0.1.7
[0.1.6]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.5...v0.1.6
[0.1.5]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.4...v0.1.5
[0.1.4]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.3...v0.1.4
[0.1.3]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.2...v0.1.3
[0.1.2]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/Automattic/jetpack-external-connections/compare/v0.1.0...v0.1.1
@@ -0,0 +1 @@
<?php return array('dependencies' => array('react-dom', 'react-jsx-runtime', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-polyfill'), 'version' => '2a33dbc0026f7152568f');
@@ -0,0 +1 @@
.jetpack-external-connection-settings{align-items:center;display:flex;gap:4px;min-height:30px}.jetpack-external-connection-settings img{width:20px}
@@ -0,0 +1 @@
.jetpack-external-connection-settings{align-items:center;display:flex;gap:4px;min-height:30px}.jetpack-external-connection-settings img{width:20px}
@@ -0,0 +1,346 @@
<?php
/**
* Management of external connections.
*
* @package automattic/jetpack-external-connections
*/
namespace Automattic\Jetpack;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Status\Host;
/**
* Main class.
*/
class External_Connections {
const PACKAGE_VERSION = '0.1.34';
const BASE_FILE = __FILE__;
/**
* List of services whose connections are managed in settings pages.
*
* Each item has a key with the slug of the settings page, and a value with an array of services.
*
* Each service has the following keys:
* - service: The service identifier.
* - title: The title of the service.
* - signup_link: The URL to the service's signup page.
* - description: The description of the service.
* - support_link: An array with the following keys:
* - jetpack: The URL handler registered in jetpack.com/redirect/.
* - wpcom: The URL of the support page for the service on WordPress.com.
* - script: An optional script handle to enqueue.
*
* @example
* ```php
* self::$services = array(
* 'media' => array(
* array(
* 'service' => 'facebook',
* 'title' => 'Facebook',
* 'signup_link' => 'https://facebook.com/signup?ref=jetpack',
* 'description' => 'Connect your site to your Facebook account',
* 'support_link' => array(
* 'jetpack' => 'facebook-connection',
* 'wpcom' => 'https://wordpress.com/support/facebook/',
* ),
* ),
* ),
* );
* ```
* @var array
*/
private static $services = array();
/**
* Gets the connect URL for a given service.
*
* @param string $service The service identifier.
* @return string|null The connect URL, or `null` if the service is not supported.
*/
public static function get_connect_url( $service ) {
if ( ( new Host() )->is_wpcom_simple() ) {
require_lib( 'external-connections' );
$connections = \WPCOM_External_Connections::init();
$service = $connections->get_external_service_item( $service );
return $service ? $service['connect_URL'] : null;
}
$site_id = Connection_Manager::get_site_id();
if ( is_wp_error( $site_id ) ) {
return null;
}
$path = sprintf( '/sites/%d/external-services', $site_id );
$response = Client::wpcom_json_api_request_as_user( $path );
if ( is_wp_error( $response ) ) {
return null;
}
$body = json_decode( wp_remote_retrieve_body( $response ) );
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
return $body->services->$service->connect_URL ?? null;
}
/**
* Retrieves a connection of the provided service.
*
* @param string $service The service identifier.
* @return array|null The connection details, or `null` if no matching connection is found.
*/
public static function get_connection( $service ) {
if ( ( new Host() )->is_wpcom_simple() ) {
require_lib( 'external-media-service/external-media-list' );
require_lib( 'external-connections' );
$connections = \WPCOM_External_Connections::init();
$token = \ExternalMediaService::get_service_token( $service, get_current_user_id() );
if ( ! empty( $token->unique_id ) ) {
// @phan-suppress-next-line PhanTypeMismatchArgument -- $token->unique_id is an int.
return $connections->get_keyring_connection_item( $token->unique_id );
}
} else {
$response = Client::wpcom_json_api_request_as_user( '/me/connections' );
if ( is_wp_error( $response ) ) {
return null;
}
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( isset( $body->connections ) && is_array( $body->connections ) ) {
foreach ( $body->connections as $connection ) {
if ( $service === $connection->service ) {
return (array) $connection;
}
}
}
}
return null;
}
/**
* Deletes a connection for the provided service.
*
* @param string $service The service identifier.
* @return boolean Whether the connection was deleted.
*/
public static function delete_connection( $service ) {
$connection = self::get_connection( $service );
if ( empty( $connection ) ) {
return false;
}
if ( ( new Host() )->is_wpcom_simple() ) {
if ( get_current_user_id() === $connection['user_ID'] ) {
require_lib( 'external-connections' );
$connections = \WPCOM_External_Connections::init();
return $connections->delete_keyring_connection( $connection['ID'] );
}
return false;
}
$response = Client::wpcom_json_api_request_as_user(
'/me/connections/' . $connection['ID'],
'2',
array( 'method' => 'DELETE' )
);
if ( is_wp_error( $response ) ) {
return false;
}
$body = json_decode( wp_remote_retrieve_body( $response ) );
return $body->deleted ?? false;
}
/**
* Gets the connection data for the provided service.
*
* @param string $service The service identifier.
* @return array The connection data.
*/
public static function get_connection_data( $service ) {
$connection = self::get_connection( $service );
if ( empty( $connection ) ) {
return array(
'is_connected' => false,
'account_name' => '',
'profile_image' => '',
);
}
$is_connected = isset( $connection['status'] ) && $connection['status'] === 'ok';
$account_name = $is_connected ? ( $connection['external_display'] ?? $connection['external_name'] ) : '';
$profile_image = $is_connected ? $connection['external_profile_picture'] : '';
return array(
'is_connected' => $is_connected,
'account_name' => $account_name,
'profile_image' => $profile_image,
);
}
/**
* Registers connection settings.
*/
public static function register_settings() {
global $pagenow;
$host = new Host();
if ( ! $host->is_wpcom_simple() ) {
$connection = new Connection_Manager( 'jetpack' );
$status = new Status();
if ( $status->is_offline_mode() || ! $connection->has_connected_owner() || ! $connection->is_user_connected() ) {
return;
}
}
foreach ( self::$services as $page => $services ) {
if ( $pagenow !== "options-$page.php" ) {
continue;
}
add_settings_section(
'jetpack_external_connections_section',
__( 'Integrations', 'jetpack-external-connections' ),
'__return_false',
$page
);
$asset_name = 'jetpack-external-connections-settings';
Assets::register_script(
$asset_name,
"build/$asset_name/$asset_name.js",
self::BASE_FILE,
array(
'in_footer' => true,
'textdomain' => 'jetpack-external-connections',
)
);
Assets::enqueue_script( $asset_name );
$script_data = array();
foreach ( $services as $service ) {
if ( $host->is_wpcom_platform() ) {
$support_link = $service['support_link']['wpcom'];
if ( function_exists( 'localized_wpcom_url' ) ) {
$support_link = localized_wpcom_url( $support_link );
}
} else {
$support_link = Redirect::get_url( $service['support_link']['jetpack'] );
}
$connect_url = self::get_connect_url( $service['service'] );
$connection_data = self::get_connection_data( $service['service'] );
if ( ( empty( $connect_url ) && ! $connection_data['is_connected'] ) ) {
continue;
}
add_settings_field(
'jetpack_external_connections_field_' . $service['service'],
$service['title'],
function () use ( $service, $support_link ) {
?>
<div class="jetpack-external-connection" data-service="<?php echo esc_attr( $service['service'] ); ?>"><em><?php esc_html_e( 'Loading…', 'jetpack-external-connections' ); ?></em></div>
<p class="description">
<?php echo esc_html( $service['description'] ); ?>
<a href="<?php echo esc_url( $support_link ); ?>" target="_blank" data-target="wpcom-help-center"><?php esc_html_e( 'Learn more', 'jetpack-external-connections' ); ?></a>
</p>
<?php
},
$page,
'jetpack_external_connections_section'
);
$script_data[ $service['service'] ] = array(
'accountName' => $connection_data['account_name'],
'connectUrl' => $connect_url,
'deleteNonce' => wp_create_nonce( 'jetpack_delete_external_connection_' . $service['service'] ),
'getNonce' => wp_create_nonce( 'jetpack_get_external_connection_' . $service['service'] ),
'isConnected' => $connection_data['is_connected'],
'profileImage' => $connection_data['profile_image'],
'supportLink' => $support_link,
'signupLink' => $service['signup_link'] ?? '',
);
if ( isset( $service['script'] ) ) {
Assets::enqueue_script( $service['script'] );
}
}
wp_add_inline_script(
$asset_name,
'const jetpackExternalConnectionsData = ' . wp_json_encode( $script_data, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';',
'before'
);
}
}
/**
* Handles the AJAX request to delete an external connection.
*/
public static function ajax_delete_connection() {
if ( ! isset( $_REQUEST['service'] ) ) {
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
wp_send_json( array( 'deleted' => 'false' ), null, JSON_UNESCAPED_SLASHES );
}
$service = sanitize_text_field( wp_unslash( $_REQUEST['service'] ) );
check_ajax_referer( 'jetpack_delete_external_connection_' . $service );
$is_deleted = self::delete_connection( $service );
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
wp_send_json( array( 'deleted' => $is_deleted ), null, JSON_UNESCAPED_SLASHES );
}
/**
* Handles the AJAX request to delete an external connection.
*/
public static function ajax_get_connection() {
if ( ! isset( $_REQUEST['service'] ) ) {
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
wp_send_json( array( 'isConnected' => 'false' ), null, JSON_UNESCAPED_SLASHES );
}
$service = sanitize_text_field( wp_unslash( $_REQUEST['service'] ) );
check_ajax_referer( 'jetpack_get_external_connection_' . $service );
$connection_data = self::get_connection_data( $service );
wp_send_json(
array(
'accountName' => $connection_data['account_name'],
'isConnected' => $connection_data['is_connected'],
'profileImage' => $connection_data['profile_image'],
),
null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
JSON_UNESCAPED_SLASHES
);
}
/**
* Registers settings and hooks for a specified service on a given admin page.
*
* @param string $page The identifier of the admin page where the service settings are added.
* @param array $service The service to be associated with the specified admin page.
*/
public static function add_settings_for_service( $page, $service ) {
self::$services[ $page ][] = $service;
if ( ! has_action( 'admin_init', array( __CLASS__, 'register_settings' ) ) ) {
add_action( 'admin_init', array( __CLASS__, 'register_settings' ), 15 );
}
if ( ! has_action( 'wp_ajax_jetpack_delete_external_connection', array( __CLASS__, 'ajax_delete_connection' ) ) ) {
add_action( 'wp_ajax_jetpack_delete_external_connection', array( __CLASS__, 'ajax_delete_connection' ) );
}
if ( ! has_action( 'wp_ajax_jetpack_get_external_connection', array( __CLASS__, 'ajax_get_connection' ) ) ) {
add_action( 'wp_ajax_jetpack_get_external_connection', array( __CLASS__, 'ajax_get_connection' ) );
}
}
}
@@ -0,0 +1,136 @@
/* global jetpackExternalConnectionsData */
import requestExternalAccess from '@automattic/request-external-access';
import domReady from '@wordpress/dom-ready';
import { useState } from '@wordpress/element';
import { applyFilters } from '@wordpress/hooks';
import { __ } from '@wordpress/i18n';
import { createRoot } from 'react-dom/client';
const fetchData = async params => {
const response = await fetch( '/wp-admin/admin-ajax.php', {
method: 'POST',
body: new URLSearchParams( params ),
headers: {
Accept: 'application/json',
},
} );
return await response.json();
};
/**
* Represents an external connection component for a given service,
* allowing users to connect or disconnect from it.
*
* @param {object} props - The properties for the ExternalConnection component.
* @param {string} props.service - The name of the service to be connected or disconnected.
* @return {import('react').Component} The ExternalConnection component.
*/
function ExternalConnection( { service } ) {
const config = jetpackExternalConnectionsData[ service ];
const [ isConnected, setIsConnected ] = useState( config.isConnected );
const [ accountName, setAccountName ] = useState( config.accountName );
const [ profileImage, setProfileImage ] = useState( config.profileImage );
const [ isTogglingConnection, setTogglingConnection ] = useState( false );
const toggleConnection = async () => {
setTogglingConnection( true );
if ( isConnected ) {
const data = await fetchData( {
action: 'jetpack_delete_external_connection',
service,
_ajax_nonce: config.deleteNonce,
} );
setTogglingConnection( false );
if ( data.deleted ) {
setIsConnected( false );
}
return;
}
requestExternalAccess( config.connectUrl, async ( { keyring_id: keyringId } ) => {
if ( ! keyringId ) {
setTogglingConnection( false );
}
const data = await fetchData( {
action: 'jetpack_get_external_connection',
service,
_ajax_nonce: config.getNonce,
} );
setIsConnected( data.isConnected );
setAccountName( data.accountName );
setProfileImage( data.profileImage );
setTogglingConnection( false );
} );
};
let buttonText;
if ( isTogglingConnection ) {
buttonText = isConnected
? __( 'Disconnecting…', 'jetpack-external-connections' )
: __(
'Connecting…',
'jetpack-external-connections',
/* dummy arg to avoid bad minification */ 0
);
} else {
buttonText = isConnected
? __( 'Disconnect', 'jetpack-external-connections' )
: __(
'Connect',
'jetpack-external-connections',
/* dummy arg to avoid bad minification */ 0
);
}
const ExtraSettings = applyFilters( 'jetpack.externalConnections.extraSettings', null, service );
return (
<>
<div className="jetpack-external-connection-settings">
{ isConnected && accountName && (
<>
{ __( 'Connected as:', 'jetpack-external-connections' ) }
{ profileImage && <img src={ profileImage } alt="" /> }
<strong>{ accountName } </strong>
</>
) }
{ ! isConnected && config.signupLink && (
<a
className={ `button-secondary ${ isTogglingConnection ? 'disabled' : '' }` }
href={ config.signupLink }
target="_blank"
rel="noopener noreferrer"
>
{ __( 'Sign up', 'jetpack-external-connections' ) }
</a>
) }
<button
className="button-secondary"
type="button"
onClick={ toggleConnection }
disabled={ isTogglingConnection }
>
{ buttonText }
</button>
</div>
{ ExtraSettings && <ExtraSettings isConnected={ isConnected } /> }
</>
);
}
domReady( () => {
const connections = document.querySelectorAll( '.jetpack-external-connection' );
if ( ! connections.length ) {
return;
}
connections.forEach( connection => {
const root = createRoot( connection );
const service = connection.dataset.service;
root.render( <ExternalConnection service={ service } /> );
} );
} );