initial
This commit is contained in:
+79
@@ -0,0 +1,79 @@
|
||||
# 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.9] - 2026-06-29
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.1.8] - 2026-06-25
|
||||
### Changed
|
||||
- Update package dependencies. [#49831]
|
||||
|
||||
## [0.1.7] - 2026-06-22
|
||||
### Changed
|
||||
- Update package dependencies. [#49631] [#49691] [#49757]
|
||||
|
||||
## [0.1.6] - 2026-06-15
|
||||
### Changed
|
||||
- Update package dependencies. [#49273] [#49492]
|
||||
|
||||
## [0.1.5] - 2026-06-08
|
||||
### Changed
|
||||
- Update dependencies. [#49354]
|
||||
|
||||
## [0.1.4] - 2026-06-01
|
||||
### Added
|
||||
- Notices: Add notice when the connection state prevents logs from showing. [#48858]
|
||||
|
||||
### Changed
|
||||
- Update package dependencies. [#48404]
|
||||
|
||||
## [0.1.3] - 2026-05-25
|
||||
### Changed
|
||||
- Update package dependencies. [#48405]
|
||||
|
||||
### Fixed
|
||||
- Fix upgrade prompt incorrectly shown to sites entitled to the full activity log. [#49067]
|
||||
|
||||
## [0.1.2] - 2026-05-19
|
||||
### Added
|
||||
- Add a "Performed by" filter for narrowing the log by actor, including MCP agents. Filtering is applied server-side so totals and pagination stay correct. [#48594]
|
||||
|
||||
### Changed
|
||||
- Update dependencies. [#48778]
|
||||
|
||||
## [0.1.1] - 2026-05-11
|
||||
### Changed
|
||||
- Activity Log: Open the Jetpack Cloud Backup restore flow from the "Manage backup" row action instead of showing a disabled placeholder. [#48531]
|
||||
- Activity Log: Refresh the free-tier upsell illustration to match Jetpack's branding. [#48531]
|
||||
- Activity Log: Rename the row action to "Restore backup" so the label matches what clicking it actually does. [#48531]
|
||||
- Components: Use Link from `@wordpress/ui` instead of ExternalLink. [#48529]
|
||||
|
||||
## [0.1.0] - 2026-05-04
|
||||
### Added
|
||||
- Initial release of the Activity Log package: Hosts the Activity Log UI and its REST endpoints directly in WP Admin. [#48244]
|
||||
|
||||
### Changed
|
||||
- Activity Log: Opt into `<AdminPage unwrapped>` so DataViews can fill the bounded content slot and scroll its table body internally. Header, date picker, and DataViews toolbar stay pinned on short viewports. [#48244]
|
||||
|
||||
### Fixed
|
||||
- Activity Log: Default the page to the Table layout, load the upsell-callout stylesheet from the main entry, and surface the disabled toolbar + disabled date-range picker on the free tier with upgrade tooltips. [#48418]
|
||||
|
||||
## 0.1.0-alpha - unreleased
|
||||
|
||||
Initial release.
|
||||
|
||||
[0.1.9]: https://github.com/Automattic/jetpack-activity-log/compare/v0.1.8...v0.1.9
|
||||
[0.1.8]: https://github.com/Automattic/jetpack-activity-log/compare/v0.1.7...v0.1.8
|
||||
[0.1.7]: https://github.com/Automattic/jetpack-activity-log/compare/v0.1.6...v0.1.7
|
||||
[0.1.6]: https://github.com/Automattic/jetpack-activity-log/compare/v0.1.5...v0.1.6
|
||||
[0.1.5]: https://github.com/Automattic/jetpack-activity-log/compare/v0.1.4...v0.1.5
|
||||
[0.1.4]: https://github.com/Automattic/jetpack-activity-log/compare/v0.1.3...v0.1.4
|
||||
[0.1.3]: https://github.com/Automattic/jetpack-activity-log/compare/v0.1.2...v0.1.3
|
||||
[0.1.2]: https://github.com/Automattic/jetpack-activity-log/compare/v0.1.1...v0.1.2
|
||||
[0.1.1]: https://github.com/Automattic/jetpack-activity-log/compare/v0.1.0...v0.1.1
|
||||
[0.1.0]: https://github.com/Automattic/jetpack-activity-log/compare/v0.1.0-alpha...v0.1.0
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<svg width="399" height="244" viewBox="0 0 399 244" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="399" height="244" fill="#003010"/>
|
||||
<path d="M181.201 141.678C191.745 152.222 208.841 152.222 219.385 141.678C229.929 131.134 229.929 114.038 219.385 103.494C208.841 92.9499 191.745 92.9499 181.201 103.494C170.657 114.038 170.657 131.134 181.201 141.678Z" stroke="url(#paint0_linear_6208_69986)"/>
|
||||
<path d="M172.716 150.163C187.946 165.394 212.64 165.394 227.87 150.163C243.101 134.933 243.101 110.239 227.87 95.0088C212.64 79.7784 187.946 79.7784 172.716 95.0088C157.485 110.239 157.485 134.933 172.716 150.163Z" stroke="url(#paint1_linear_6208_69986)"/>
|
||||
<path d="M164.231 158.648C184.147 178.565 216.439 178.565 236.355 158.648C256.272 138.732 256.272 106.44 236.355 86.5236C216.439 66.6068 184.147 66.6068 164.231 86.5236C144.314 106.44 144.314 138.732 164.231 158.648Z" stroke="url(#paint2_linear_6208_69986)"/>
|
||||
<path d="M155.745 167.134C180.348 191.737 220.238 191.737 244.841 167.134C269.444 142.531 269.444 102.641 244.841 78.0383C220.238 53.4352 180.348 53.4352 155.745 78.0383C131.142 102.641 131.142 142.531 155.745 167.134Z" stroke="url(#paint3_linear_6208_69986)"/>
|
||||
<path d="M147.26 175.619C176.549 204.908 224.037 204.908 253.326 175.619C282.615 146.33 282.615 98.8423 253.326 69.553C224.037 40.2636 176.549 40.2636 147.26 69.553C117.971 98.8423 117.971 146.33 147.26 175.619Z" stroke="url(#paint4_linear_6208_69986)"/>
|
||||
<path d="M200 123V46.053C200 46.053 216.757 44.923 234.595 53.0974C253.514 61.7673 259.948 70.5405 268.649 83.4428C278.072 97.4179 280 123 280 123H200Z" fill="url(#paint5_linear_6208_69986)"/>
|
||||
<path d="M303.5 18.5V225.5H96.5V18.5H303.5Z" stroke="#069E08"/>
|
||||
<path d="M303 24.25C305.347 24.25 307.25 22.3472 307.25 20C307.25 17.6528 305.347 15.75 303 15.75C300.653 15.75 298.75 17.6528 298.75 20C298.75 22.3472 300.653 24.25 303 24.25Z" fill="#003010" stroke="#069E08" stroke-width="1.5"/>
|
||||
<path d="M303 230.25C305.347 230.25 307.25 228.347 307.25 226C307.25 223.653 305.347 221.75 303 221.75C300.653 221.75 298.75 223.653 298.75 226C298.75 228.347 300.653 230.25 303 230.25Z" fill="#003010" stroke="#069E08" stroke-width="1.5"/>
|
||||
<path d="M97 23.25C99.3472 23.25 101.25 21.3472 101.25 19C101.25 16.6528 99.3472 14.75 97 14.75C94.6528 14.75 92.75 16.6528 92.75 19C92.75 21.3472 94.6528 23.25 97 23.25Z" fill="#003010" stroke="#069E08" stroke-width="1.5"/>
|
||||
<path d="M97 229.25C99.3472 229.25 101.25 227.347 101.25 225C101.25 222.653 99.3472 220.75 97 220.75C94.6528 220.75 92.75 222.653 92.75 225C92.75 227.347 94.6528 229.25 97 229.25Z" fill="#003010" stroke="#069E08" stroke-width="1.5"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_6208_69986" x1="219.385" y1="141.678" x2="181.201" y2="103.494" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2E38FA"/>
|
||||
<stop offset="0.528154" stop-color="#48FF50"/>
|
||||
<stop offset="1" stop-color="#DAFFDC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_6208_69986" x1="227.87" y1="150.163" x2="172.716" y2="95.0088" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2E38FA"/>
|
||||
<stop offset="0.528154" stop-color="#48FF50"/>
|
||||
<stop offset="1" stop-color="#DAFFDC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_6208_69986" x1="236.355" y1="158.648" x2="164.231" y2="86.5236" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2E38FA"/>
|
||||
<stop offset="0.528154" stop-color="#48FF50"/>
|
||||
<stop offset="1" stop-color="#DAFFDC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_6208_69986" x1="244.841" y1="167.134" x2="155.745" y2="78.0383" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2E38FA"/>
|
||||
<stop offset="0.528154" stop-color="#48FF50"/>
|
||||
<stop offset="1" stop-color="#DAFFDC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_6208_69986" x1="253.326" y1="175.619" x2="147.26" y2="69.553" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2E38FA"/>
|
||||
<stop offset="0.528154" stop-color="#48FF50"/>
|
||||
<stop offset="1" stop-color="#DAFFDC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_6208_69986" x1="240.289" y1="121.937" x2="221.82" y2="98.8034" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#003010" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#003010"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
+17
@@ -0,0 +1,17 @@
|
||||
<svg width="380" height="183" viewBox="0 0 380 183" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.75" filter="url(#filter0_f_5953_11924)">
|
||||
<path d="M360 -60C360 7.93102 304.931 63 237 63C169.069 63 114 7.93102 114 -60C114 -127.931 169.069 -183 237 -183C304.931 -183 360 -127.931 360 -60Z" fill="url(#paint0_linear_5953_11924)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_5953_11924" x="-6" y="-303" width="486" height="486" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="60" result="effect1_foregroundBlur_5953_11924"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_5953_11924" x1="360" y1="-60" x2="114" y2="-60" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#48FF50"/>
|
||||
<stop offset="0.471846" stop-color="#108642"/>
|
||||
<stop offset="1" stop-color="#2E38FA"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 990 B |
+1
@@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('jetpack-connection', 'jetpack-script-data', 'react', 'react-dom', 'react-jsx-runtime', 'wp-a11y', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n', 'wp-keycodes', 'wp-polyfill', 'wp-primitives', 'wp-private-apis', 'wp-url', 'wp-warning'), 'version' => '8dd753bb5b65f7782f75');
|
||||
+1
File diff suppressed because one or more lines are too long
+115
File diff suppressed because one or more lines are too long
+9
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @license React
|
||||
* use-sync-external-store-shim.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
+1
File diff suppressed because one or more lines are too long
+84
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/**
|
||||
* The Activity Log React initial state.
|
||||
*
|
||||
* @package automattic/jetpack-activity-log
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Activity_Log;
|
||||
|
||||
use Automattic\Jetpack\Status;
|
||||
use Jetpack_Options;
|
||||
use function admin_url;
|
||||
use function esc_url_raw;
|
||||
use function get_bloginfo;
|
||||
use function get_locale;
|
||||
use function get_option;
|
||||
use function get_site_url;
|
||||
use function plugins_url;
|
||||
use function rest_url;
|
||||
use function wp_create_nonce;
|
||||
use function wp_json_encode;
|
||||
use function wp_parse_url;
|
||||
|
||||
/**
|
||||
* The Activity Log React initial state.
|
||||
*/
|
||||
class Initial_State {
|
||||
/**
|
||||
* Get the initial state data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_data() {
|
||||
$gmt_offset = get_option( 'gmt_offset' );
|
||||
$timezone_string = get_option( 'timezone_string' );
|
||||
$home_host = wp_parse_url( get_site_url(), PHP_URL_HOST );
|
||||
|
||||
return array(
|
||||
'API' => array(
|
||||
'WP_API_root' => esc_url_raw( rest_url() ),
|
||||
'WP_API_nonce' => wp_create_nonce( 'wp_rest' ),
|
||||
),
|
||||
'jetpackStatus' => array(
|
||||
'calypsoSlug' => ( new Status() )->get_site_suffix(),
|
||||
),
|
||||
'siteData' => array(
|
||||
'id' => Jetpack_Options::get_option( 'id' ),
|
||||
'title' => get_bloginfo( 'name' ) ? get_bloginfo( 'name' ) : get_site_url(),
|
||||
'adminUrl' => esc_url_raw( admin_url() ),
|
||||
'slug' => is_string( $home_host ) ? $home_host : '',
|
||||
'gmtOffset' => is_numeric( $gmt_offset ) ? (float) $gmt_offset : 0.0,
|
||||
'timezoneString' => is_string( $timezone_string ) ? $timezone_string : '',
|
||||
'locale' => str_replace( '_', '-', (string) get_locale() ),
|
||||
// The paid-plan capability check. Drives the free-tier
|
||||
// upsell callout and matches the server-side clamp in
|
||||
// REST_Controller::get_activity_log(). The result is
|
||||
// cached in a site transient by the REST controller
|
||||
// (5-minute TTL); on a cache miss this call issues a
|
||||
// synchronous WPCOM request (~200–800ms typical, up to
|
||||
// the 2s internal timeout) that blocks page rendering
|
||||
// until it returns.
|
||||
'hasActivityLogsAccess' => REST_Controller::has_activity_logs_access(),
|
||||
),
|
||||
'nonces' => array(
|
||||
// Consumed by `UpsellCallout` to build a `redirect_to`
|
||||
// URL that invalidates the access cache on return from
|
||||
// checkout. See `Jetpack_Activity_Log::admin_init()`.
|
||||
'refreshAccess' => wp_create_nonce( Jetpack_Activity_Log::REFRESH_ACCESS_NONCE_ACTION ),
|
||||
),
|
||||
'assets' => array(
|
||||
'buildUrl' => plugins_url( '../build/', __FILE__ ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the initial state into a JavaScript variable.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render() {
|
||||
return 'var JPACTIVITYLOG_INITIAL_STATE=' . wp_json_encode( $this->get_data(), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';';
|
||||
}
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
/**
|
||||
* Primary class for the Jetpack Activity Log package.
|
||||
*
|
||||
* @package automattic/jetpack-activity-log
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Activity_Log;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
use Automattic\Jetpack\Activity_Log\Initial_State as Activity_Log_Initial_State;
|
||||
use Automattic\Jetpack\Admin_UI\Admin_Menu;
|
||||
use Automattic\Jetpack\Assets;
|
||||
use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use function add_action;
|
||||
use function add_filter;
|
||||
use function current_user_can;
|
||||
use function did_action;
|
||||
use function do_action;
|
||||
use function is_multisite;
|
||||
use function sanitize_text_field;
|
||||
use function wp_add_inline_script;
|
||||
use function wp_unslash;
|
||||
use function wp_verify_nonce;
|
||||
|
||||
/**
|
||||
* Class Jetpack_Activity_Log
|
||||
*
|
||||
* Registers the Activity Log admin page and its REST routes inside the
|
||||
* main Jetpack plugin.
|
||||
*/
|
||||
class Jetpack_Activity_Log {
|
||||
|
||||
/**
|
||||
* Admin page slug.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const PAGE_SLUG = 'jetpack-activity-log';
|
||||
|
||||
/**
|
||||
* Script handle for the admin bundle.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SCRIPT_HANDLE = 'jetpack-activity-log';
|
||||
|
||||
/**
|
||||
* Nonce action for refreshing the access flag after a checkout
|
||||
* return. Used by `admin_init()` below and exposed to the client via
|
||||
* Initial_State so the upsell CTA can embed a valid nonce in its
|
||||
* `redirect_to`. Same shape as `Social_Admin_Page::REFRESH_PLAN_NONCE_ACTION`.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const REFRESH_ACCESS_NONCE_ACTION = 'jetpack_activity_log_refresh_access';
|
||||
|
||||
/**
|
||||
* Entry point. Idempotent: safe to call from multiple bootstraps.
|
||||
*/
|
||||
public static function initialize() {
|
||||
if ( did_action( 'jetpack_activity_log_initialized' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action( 'admin_menu', array( __CLASS__, 'add_wp_admin_submenu' ) );
|
||||
add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
|
||||
add_filter( 'jetpack_package_versions', array( Package_Version::class, 'send_package_version_to_tracker' ) );
|
||||
|
||||
/**
|
||||
* Fires once the Jetpack Activity Log package has wired its hooks.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
do_action( 'jetpack_activity_log_initialized' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the Activity Log submenu under Jetpack.
|
||||
*
|
||||
* Mirrors the gating used by the legacy my-jetpack "Activity Log" menu
|
||||
* item (connected user + non-multisite).
|
||||
*
|
||||
* @return string|null The resulting page's hook suffix, if registered.
|
||||
*/
|
||||
public static function add_wp_admin_submenu() {
|
||||
if ( ! self::is_available() ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$page_suffix = Admin_Menu::add_menu(
|
||||
/** "Activity Log" is a product name, do not translate. */
|
||||
'Activity Log',
|
||||
'Activity Log',
|
||||
'manage_options',
|
||||
self::PAGE_SLUG,
|
||||
array( __CLASS__, 'render_page' ),
|
||||
14
|
||||
);
|
||||
|
||||
if ( $page_suffix ) {
|
||||
add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
|
||||
}
|
||||
|
||||
return $page_suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the Activity Log page should be shown to the current user.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_available() {
|
||||
if ( is_multisite() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ( new Connection_Manager() )->is_user_connected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when the admin page is loaded.
|
||||
*
|
||||
* When the user is returning from a successful checkout, the upsell
|
||||
* CTA appends `?refresh_access=1&_wpnonce=…` to the `redirect_to`
|
||||
* value it hands off to WordPress.com. Detect that here, verify the
|
||||
* nonce, and drop the cached paid-plan signal so
|
||||
* `Initial_State::get_data()` (which runs later in the same request,
|
||||
* when the bundle is enqueued) rehydrates from WPCOM instead of
|
||||
* re-serving the pre-checkout value. Mirrors the pattern in
|
||||
* `Automattic\Jetpack\Publicize\Social_Admin_Page::admin_init()`.
|
||||
*/
|
||||
public static function admin_init() {
|
||||
if ( isset( $_GET['refresh_access'] ) && isset( $_GET['_wpnonce'] ) ) {
|
||||
$nonce = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) );
|
||||
if ( wp_verify_nonce( $nonce, self::REFRESH_ACCESS_NONCE_ACTION ) ) {
|
||||
REST_Controller::clear_access_cache();
|
||||
}
|
||||
}
|
||||
|
||||
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_admin_scripts' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the admin bundle and seed initial state.
|
||||
*/
|
||||
public static function enqueue_admin_scripts() {
|
||||
Assets::register_script(
|
||||
self::SCRIPT_HANDLE,
|
||||
'../build/index.js',
|
||||
__FILE__,
|
||||
array(
|
||||
'in_footer' => true,
|
||||
'textdomain' => 'jetpack-activity-log',
|
||||
)
|
||||
);
|
||||
Assets::enqueue_script( self::SCRIPT_HANDLE );
|
||||
|
||||
wp_add_inline_script( self::SCRIPT_HANDLE, ( new Activity_Log_Initial_State() )->render(), 'before' );
|
||||
Connection_Initial_State::render_script( self::SCRIPT_HANDLE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the admin page root node. React mounts into this element.
|
||||
*/
|
||||
public static function render_page() {
|
||||
?>
|
||||
<div id="jetpack-activity-log-root"></div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the REST routes backing the Activity Log UI.
|
||||
*
|
||||
* Routes are added in Phase 2. This method exists now so that the
|
||||
* `jetpack/v4/activity-log` namespace is reserved and the hook is wired.
|
||||
*/
|
||||
public static function register_rest_routes() {
|
||||
REST_Controller::register_rest_routes();
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* The Package_Version class.
|
||||
*
|
||||
* @package automattic/jetpack-activity-log
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Activity_Log;
|
||||
|
||||
/**
|
||||
* The Package_Version class.
|
||||
*
|
||||
* Does *not* use namespaced versioning ("VXXXX") because send_package_version_to_tracker() is used as a
|
||||
* "jetpack_package_versions" filter, and said filter gets run during a plugin upgrade, so it always expects to
|
||||
* find the "Package_Version" class with the same namespace, name, and interface.
|
||||
*/
|
||||
class Package_Version {
|
||||
|
||||
const PACKAGE_VERSION = '0.1.9';
|
||||
|
||||
const PACKAGE_SLUG = 'activity-log';
|
||||
|
||||
/**
|
||||
* Adds the package slug and version to the package version tracker's data.
|
||||
*
|
||||
* @param array $package_versions The package version array.
|
||||
*
|
||||
* @return array The package version array.
|
||||
*/
|
||||
public static function send_package_version_to_tracker( $package_versions ) {
|
||||
$package_versions[ self::PACKAGE_SLUG ] = self::PACKAGE_VERSION;
|
||||
|
||||
return $package_versions;
|
||||
}
|
||||
}
|
||||
+473
@@ -0,0 +1,473 @@
|
||||
<?php
|
||||
/**
|
||||
* The Activity Log REST Controller.
|
||||
*
|
||||
* Registers the `/jetpack/v4/activity-log/*` routes backing the admin
|
||||
* UI. Each route is a thin proxy to the corresponding WPCOM v2
|
||||
* endpoint, authenticated with the site's blog token.
|
||||
*
|
||||
* @package automattic/jetpack-activity-log
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Activity_Log;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Automattic\Jetpack\Status\Visitor;
|
||||
use Jetpack_Options;
|
||||
use WP_Error;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Server;
|
||||
use function current_user_can;
|
||||
use function delete_site_transient;
|
||||
use function esc_html__;
|
||||
use function get_site_transient;
|
||||
use function http_build_query;
|
||||
use function is_wp_error;
|
||||
use function json_decode;
|
||||
use function register_rest_route;
|
||||
use function rest_ensure_response;
|
||||
use function set_site_transient;
|
||||
use function wp_remote_retrieve_body;
|
||||
use function wp_remote_retrieve_response_code;
|
||||
|
||||
/**
|
||||
* REST routes for the Activity Log UI.
|
||||
*/
|
||||
class REST_Controller {
|
||||
|
||||
/**
|
||||
* REST namespace used by this package.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const REST_NAMESPACE = 'jetpack/v4';
|
||||
|
||||
/**
|
||||
* Max items returned per request on the free tier. Matches the "20 most
|
||||
* recent events" copy used by Calypso's upsell callout.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const FREE_TIER_ITEM_CAP = 20;
|
||||
|
||||
/**
|
||||
* Site-transient TTL for the has-access capability check.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const CAPABILITY_CACHE_TTL = 5 * MINUTE_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* Transient key prefix for the per-blog access cache.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CAPABILITY_CACHE_KEY = 'jetpack_activity_log_has_access_';
|
||||
|
||||
/**
|
||||
* Query params accepted by the list endpoint. Shape matches Calypso's
|
||||
* ActivityLogParams so the ported UI can forward its filter state
|
||||
* verbatim.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function list_args() {
|
||||
return array(
|
||||
'number' => array(
|
||||
'description' => __( 'Number of items to return per page.', 'jetpack-activity-log' ),
|
||||
'type' => 'integer',
|
||||
'minimum' => 1,
|
||||
'maximum' => 1000,
|
||||
),
|
||||
'page' => array(
|
||||
'description' => __( '1-indexed page number.', 'jetpack-activity-log' ),
|
||||
'type' => 'integer',
|
||||
'minimum' => 1,
|
||||
),
|
||||
'sort_order' => array(
|
||||
'description' => __( 'Sort direction.', 'jetpack-activity-log' ),
|
||||
'type' => 'string',
|
||||
'enum' => array( 'asc', 'desc' ),
|
||||
),
|
||||
'after' => array(
|
||||
'description' => __( 'ISO 8601 lower bound on event timestamp.', 'jetpack-activity-log' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
),
|
||||
'before' => array(
|
||||
'description' => __( 'ISO 8601 upper bound on event timestamp.', 'jetpack-activity-log' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
),
|
||||
'group' => array(
|
||||
'description' => __( 'Only return events in these groups.', 'jetpack-activity-log' ),
|
||||
'type' => 'array',
|
||||
'items' => array( 'type' => 'string' ),
|
||||
),
|
||||
'not_group' => array(
|
||||
'description' => __( 'Exclude events in these groups.', 'jetpack-activity-log' ),
|
||||
'type' => 'array',
|
||||
'items' => array( 'type' => 'string' ),
|
||||
),
|
||||
'text_search' => array(
|
||||
'description' => __( 'Full-text search string.', 'jetpack-activity-log' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'actor' => array(
|
||||
'description' => __( 'Only return events performed by these actor IDs.', 'jetpack-activity-log' ),
|
||||
'type' => 'array',
|
||||
'items' => array( 'type' => 'string' ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query params accepted by the actors endpoint. Same date window as the
|
||||
* counts endpoint (no pagination, no sort, no filters) — we just want
|
||||
* the distinct set for the "Performed by" dropdown.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function actors_args() {
|
||||
return array(
|
||||
'number' => array(
|
||||
'description' => __( 'Cap on the number of events considered when collecting actors.', 'jetpack-activity-log' ),
|
||||
'type' => 'integer',
|
||||
'minimum' => 1,
|
||||
'maximum' => 1000,
|
||||
),
|
||||
'after' => array(
|
||||
'description' => __( 'ISO 8601 lower bound on event timestamp.', 'jetpack-activity-log' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
),
|
||||
'before' => array(
|
||||
'description' => __( 'ISO 8601 upper bound on event timestamp.', 'jetpack-activity-log' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query params accepted by the group-counts endpoint. A subset of the
|
||||
* list params — no pagination or sort, no text search.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function group_counts_args() {
|
||||
return array(
|
||||
'number' => array(
|
||||
'description' => __( 'Cap on the number of events considered when counting groups.', 'jetpack-activity-log' ),
|
||||
'type' => 'integer',
|
||||
'minimum' => 1,
|
||||
'maximum' => 1000,
|
||||
),
|
||||
'after' => array(
|
||||
'description' => __( 'ISO 8601 lower bound on event timestamp.', 'jetpack-activity-log' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
),
|
||||
'before' => array(
|
||||
'description' => __( 'ISO 8601 upper bound on event timestamp.', 'jetpack-activity-log' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
),
|
||||
'group' => array(
|
||||
'description' => __( 'Only count events in these groups.', 'jetpack-activity-log' ),
|
||||
'type' => 'array',
|
||||
'items' => array( 'type' => 'string' ),
|
||||
),
|
||||
'not_group' => array(
|
||||
'description' => __( 'Exclude events in these groups.', 'jetpack-activity-log' ),
|
||||
'type' => 'array',
|
||||
'items' => array( 'type' => 'string' ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the Activity Log REST routes.
|
||||
*
|
||||
* Hooked on `rest_api_init` by {@see Jetpack_Activity_Log::initialize()}.
|
||||
*/
|
||||
public static function register_rest_routes() {
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/activity-log',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( __CLASS__, 'get_activity_log' ),
|
||||
'permission_callback' => array( __CLASS__, 'permissions_callback' ),
|
||||
'args' => self::list_args(),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/activity-log/count/group',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( __CLASS__, 'get_activity_log_group_counts' ),
|
||||
'permission_callback' => array( __CLASS__, 'permissions_callback' ),
|
||||
'args' => self::group_counts_args(),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/activity-log/actors',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( __CLASS__, 'get_activity_log_actors' ),
|
||||
'permission_callback' => array( __CLASS__, 'permissions_callback' ),
|
||||
'args' => self::actors_args(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission callback. Mirrors the menu gating — any admin on a
|
||||
* non-multisite install with a user-level WPCOM connection can read
|
||||
* the log. A user-level connection is required because the upstream
|
||||
* WPCOM endpoint is user-gated (it needs to identify *which* admin
|
||||
* is asking); signing as the blog gets rejected with "Only
|
||||
* Administrators can query information about the current site."
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public static function permissions_callback() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! ( new Connection_Manager() )->is_user_connected() ) {
|
||||
return new WP_Error(
|
||||
'activity_log_user_not_connected',
|
||||
esc_html__( 'Your WordPress.com account is not connected to this site. Connect it to use the Activity Log.', 'jetpack-activity-log' ),
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the site's current plan unlocks the full activity log.
|
||||
*
|
||||
* Checks the WPCOM `/sites/{id}/features` endpoint for the
|
||||
* `full-activity-log` feature flag and caches the boolean for
|
||||
* {@see self::CAPABILITY_CACHE_TTL} seconds in a site transient so the
|
||||
* list endpoint doesn't pay the round-trip on every pagination page.
|
||||
* The cache is per-blog (fine for multisite) and keyed on `blog_id`.
|
||||
*
|
||||
* Checking the feature flag (rather than a specific plan slug or the
|
||||
* rewind state) means Jetpack Complete, Security, Personal, and all
|
||||
* standalone Backup plans are covered correctly, regardless of whether
|
||||
* backup credentials have been configured.
|
||||
*
|
||||
* @return bool True when the site has the full-activity-log feature.
|
||||
*/
|
||||
public static function has_activity_logs_access() {
|
||||
$blog_id = (int) Jetpack_Options::get_option( 'id' );
|
||||
if ( ! $blog_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cache_key = self::CAPABILITY_CACHE_KEY . $blog_id;
|
||||
$cached = get_site_transient( $cache_key );
|
||||
if ( false !== $cached ) {
|
||||
return 'yes' === $cached;
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_blog(
|
||||
sprintf( '/sites/%d/features', $blog_id ),
|
||||
'1.1',
|
||||
array( 'timeout' => 2 )
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
|
||||
// Fail closed: assume no access if we can't reach WPCOM. Cache for
|
||||
// a short window to avoid hammering the endpoint on every call.
|
||||
set_site_transient( $cache_key, 'no', 10 );
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
$active = is_object( $body ) && isset( $body->active ) && is_array( $body->active ) ? $body->active : array();
|
||||
$has_it = in_array( 'full-activity-log', $active, true );
|
||||
set_site_transient( $cache_key, $has_it ? 'yes' : 'no', self::CAPABILITY_CACHE_TTL );
|
||||
return $has_it;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cached has-access flag. Exposed so front-end flows that
|
||||
* know the plan just changed (e.g. a successful checkout redirect) can
|
||||
* force a refresh on the next request.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function clear_access_cache() {
|
||||
$blog_id = (int) Jetpack_Options::get_option( 'id' );
|
||||
if ( $blog_id ) {
|
||||
delete_site_transient( self::CAPABILITY_CACHE_KEY . $blog_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Free-tier params that survive the filter strip in
|
||||
* {@see self::get_activity_log()}. Anything outside this list is
|
||||
* nulled out before the request reaches WPCOM.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
const FREE_TIER_ALLOWED_PARAMS = array( 'number', 'page', 'sort_order' );
|
||||
|
||||
/**
|
||||
* Proxy the paginated activity list.
|
||||
*
|
||||
* Enforces the free-tier boundary server-side. When the site doesn't
|
||||
* have access:
|
||||
*
|
||||
* 1. `number` is clamped to {@see self::FREE_TIER_ITEM_CAP}.
|
||||
* 2. `page` is forced to 1.
|
||||
* 3. All filter inputs (`after`, `before`, `group`, `not_group`,
|
||||
* `text_search`, `actor`) are dropped.
|
||||
*
|
||||
* Together these mean a client-side bypass (DevTools, direct
|
||||
* `wp.apiFetch`) is bounded to "the 20 most recent events overall" —
|
||||
* the same dataset the locked-down UI surfaces. Without (3),
|
||||
* date-walking via `before` would let a free-tier caller page through
|
||||
* the entire history 20 rows at a time.
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_activity_log( WP_REST_Request $request ) {
|
||||
if ( ! self::has_activity_logs_access() ) {
|
||||
// Mutating the request in-place is deliberate: any
|
||||
// downstream `rest_request_*` filter sees the clamped
|
||||
// values, not the caller's originals. For a security
|
||||
// clamp that's the right side of the trade — no filter
|
||||
// can undo the limit.
|
||||
$requested = (int) $request->get_param( 'number' );
|
||||
$request->set_param(
|
||||
'number',
|
||||
$requested > 0 ? min( $requested, self::FREE_TIER_ITEM_CAP ) : self::FREE_TIER_ITEM_CAP
|
||||
);
|
||||
$request->set_param( 'page', 1 );
|
||||
|
||||
foreach ( array_keys( self::list_args() ) as $key ) {
|
||||
if ( ! in_array( $key, self::FREE_TIER_ALLOWED_PARAMS, true ) ) {
|
||||
$request->set_param( $key, null );
|
||||
}
|
||||
}
|
||||
}
|
||||
return self::proxy_get( '/activity', $request, array_keys( self::list_args() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy the group-counts endpoint.
|
||||
*
|
||||
* Deliberately not tier-clamped — the free-tier list clamp
|
||||
* (`number` → 20 / `page` → 1) is the security boundary; the group
|
||||
* counts are cosmetic metadata that powers the filter dropdown. A
|
||||
* stable, full-history count keeps the dropdown from flickering as
|
||||
* users type in the search field, matching Calypso's behavior at
|
||||
* `wp-calypso:client/dashboard/sites/logs-activity/dataviews/
|
||||
* index.tsx:100-102`.
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_activity_log_group_counts( WP_REST_Request $request ) {
|
||||
return self::proxy_get( '/activity/count/group', $request, array_keys( self::group_counts_args() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy the actors endpoint. Returns the distinct actors that have at
|
||||
* least one event in the requested date window — used to populate the
|
||||
* "Performed by" filter dropdown.
|
||||
*
|
||||
* Tier-clamping mirrors the group-counts endpoint: the list clamp at
|
||||
* {@see self::get_activity_log()} is the security boundary, so the
|
||||
* actors metadata is fine to serve unconditionally.
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_activity_log_actors( WP_REST_Request $request ) {
|
||||
return self::proxy_get( '/activity/actors', $request, array_keys( self::actors_args() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared helper: forward whitelisted query params from $request to the
|
||||
* equivalent WPCOM v2 path under `/sites/{blog_id}`.
|
||||
*
|
||||
* @param string $wpcom_path Path relative to the site, starting with "/".
|
||||
* @param WP_REST_Request $request Incoming request.
|
||||
* @param array $allowed_keys Params to forward. Any unset keys are dropped.
|
||||
* @return mixed Decoded JSON response from WPCOM, or WP_Error on failure.
|
||||
*/
|
||||
private static function proxy_get( $wpcom_path, WP_REST_Request $request, array $allowed_keys ) {
|
||||
$blog_id = Jetpack_Options::get_option( 'id' );
|
||||
if ( ! $blog_id ) {
|
||||
return new WP_Error(
|
||||
'activity_log_not_connected',
|
||||
esc_html__( 'This site is not connected to WordPress.com.', 'jetpack-activity-log' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
$params = array();
|
||||
foreach ( $allowed_keys as $key ) {
|
||||
$value = $request->get_param( $key );
|
||||
if ( $value !== null ) {
|
||||
$params[ $key ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$path = sprintf( '/sites/%d%s', (int) $blog_id, $wpcom_path );
|
||||
if ( ! empty( $params ) ) {
|
||||
$path .= '?' . http_build_query( $params );
|
||||
}
|
||||
|
||||
// Sign as the current user, not the blog: the upstream /sites/{id}/activity
|
||||
// endpoint checks that a specific admin is asking. Forward the visitor IP
|
||||
// so WPCOM logs match the existing /jetpack/v4/site/activity proxy.
|
||||
$response = Client::wpcom_json_api_request_as_user(
|
||||
$path,
|
||||
'2',
|
||||
array(
|
||||
'method' => 'GET',
|
||||
'headers' => array(
|
||||
'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
|
||||
),
|
||||
),
|
||||
null,
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return new WP_Error(
|
||||
'activity_log_request_failed',
|
||||
$response->get_error_message(),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
$status = (int) wp_remote_retrieve_response_code( $response );
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
if ( 200 !== $status ) {
|
||||
return new WP_Error(
|
||||
'activity_log_request_failed',
|
||||
isset( $body['message'] ) ? (string) $body['message'] : esc_html__( 'Unable to fetch activity log.', 'jetpack-activity-log' ),
|
||||
array( 'status' => $status ? $status : 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response( $body );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user