Skip to content

Commit b61a0c4

Browse files
sergeymitrjeherve
andauthored
Stats: add 'stats/blog' REST endpoint (#36571)
* Stats: add 'stats/blog' REST endpoint. * Version bump. * Fix unit tests. * Fix tests. * Fix phan. * Add package version tracking. * Add unit tests for the package version tracking. --------- Co-authored-by: Jeremy Herve <[email protected]>
1 parent 9008e3a commit b61a0c4

13 files changed

+372
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: added
3+
4+
Add the 'stats/blog' REST endpoint.

projects/packages/stats/composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"link-template": "https://github.com/Automattic/jetpack-stats/compare/v${old}...v${new}"
5151
},
5252
"branch-alias": {
53-
"dev-trunk": "0.11.x-dev"
53+
"dev-trunk": "0.12.x-dev"
5454
},
5555
"textdomain": "jetpack-stats"
5656
},

projects/packages/stats/src/class-main.php

+4
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ private function __construct() {
7575
add_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_caps' ), 10, 3 );
7676

7777
XMLRPC_Provider::init();
78+
REST_Provider::init();
79+
80+
// Set up package version hook.
81+
add_filter( 'jetpack_package_versions', __NAMESPACE__ . '\Package_Version::send_package_version_to_tracker' );
7882
}
7983

8084
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
/**
3+
* The Package_Version class.
4+
*
5+
* @package automattic/jetpack-stats
6+
*/
7+
8+
namespace Automattic\Jetpack\Stats;
9+
10+
/**
11+
* The Package_Version class.
12+
*/
13+
class Package_Version {
14+
15+
const PACKAGE_VERSION = '0.12.0-alpha';
16+
17+
const PACKAGE_SLUG = 'stats';
18+
19+
/**
20+
* Adds the package slug and version to the package version tracker's data.
21+
*
22+
* @param array $package_versions The package version array.
23+
*
24+
* @return array The package version array.
25+
*/
26+
public static function send_package_version_to_tracker( $package_versions ) {
27+
$package_versions[ self::PACKAGE_SLUG ] = self::PACKAGE_VERSION;
28+
return $package_versions;
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
/**
3+
* The Stats REST Provider class.
4+
*
5+
* @package @automattic/jetpack-stats
6+
*/
7+
8+
namespace Automattic\Jetpack\Stats;
9+
10+
use Automattic\Jetpack\Connection\Rest_Authentication;
11+
use Automattic\Jetpack\Connection\REST_Connector;
12+
use WP_Error;
13+
use WP_REST_Server;
14+
15+
/**
16+
* The REST API provider class.
17+
*
18+
* @since $$next-version$$
19+
*/
20+
class REST_Provider {
21+
/**
22+
* Singleton instance.
23+
*
24+
* @var REST_Provider
25+
**/
26+
private static $instance = null;
27+
28+
/**
29+
* Private constructor.
30+
*
31+
* Use the static::init() method to get an instance.
32+
*/
33+
private function __construct() {
34+
add_action( 'rest_api_init', array( $this, 'initialize_rest_api' ) );
35+
}
36+
37+
/**
38+
* Initialize class and get back a singleton instance.
39+
*
40+
* @param bool $new_instance Force create new instance.
41+
*
42+
* @return static
43+
*/
44+
public static function init( $new_instance = false ) {
45+
if ( null === self::$instance || $new_instance ) {
46+
self::$instance = new static();
47+
}
48+
49+
return self::$instance;
50+
}
51+
52+
/**
53+
* Initialize the REST API.
54+
*
55+
* @return void
56+
*/
57+
public function initialize_rest_api() {
58+
register_rest_route(
59+
'jetpack/v4',
60+
'/stats/blog',
61+
array(
62+
'methods' => WP_REST_Server::READABLE,
63+
'callback' => array( $this, 'get_blog' ),
64+
'permission_callback' => array( $this, 'get_blog_permission_check' ),
65+
)
66+
);
67+
}
68+
69+
/**
70+
* Get the stats blog data.
71+
*
72+
* @since $$next-version$$
73+
*
74+
* @return array
75+
*/
76+
public function get_blog() {
77+
return XMLRPC_Provider::init()->get_blog();
78+
}
79+
80+
/**
81+
* Check permissions for the `/stats/blog` endpoint.
82+
*
83+
* @return WP_Error|true
84+
*/
85+
public function get_blog_permission_check() {
86+
return Rest_Authentication::is_signed_with_blog_token()
87+
? true
88+
: new WP_Error( 'invalid_permission_stats_get_blog', REST_Connector::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
89+
}
90+
}

projects/packages/stats/src/class-xmlrpc-provider.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@ private function __construct() {
3838
/**
3939
* Initialize class and get back a singleton instance.
4040
*
41+
* @param bool $new_instance Force create new instance.
42+
*
4143
* @return XMLRPC_Provider
4244
*/
43-
public static function init() {
44-
if ( null === self::$instance ) {
45+
public static function init( $new_instance = false ) {
46+
if ( null === self::$instance || $new_instance ) {
4547
self::$instance = new XMLRPC_Provider();
4648
}
4749

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2+
3+
namespace Automattic\Jetpack\Stats;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
/**
8+
* Unit tests for the Package_Version class.
9+
*
10+
* @package automattic/jetpack-stats
11+
*/
12+
class Test_Package_Version extends TestCase {
13+
14+
/**
15+
* Tests that the stats package version is added to the package versions array obtained by the
16+
* Package_Version_Tracker.
17+
*/
18+
public function test_send_package_version_to_tracker_empty_array() {
19+
$expected = array(
20+
Package_Version::PACKAGE_SLUG => Package_Version::PACKAGE_VERSION,
21+
);
22+
23+
Main::init();
24+
25+
$this->assertSame( $expected, apply_filters( 'jetpack_package_versions', array() ) );
26+
}
27+
28+
/**
29+
* Tests that the stats package version is added to the package versions array obtained by the
30+
* Package_Version_Tracker.
31+
*/
32+
public function test_send_package_version_to_tracker_existing_array() {
33+
$existing_array = array(
34+
'test-package-slug' => '1.0.0',
35+
);
36+
37+
$expected = array_merge(
38+
$existing_array,
39+
array( Package_Version::PACKAGE_SLUG => Package_Version::PACKAGE_VERSION )
40+
);
41+
42+
add_filter( 'jetpack_package_versions', __NAMESPACE__ . '\Package_Version::send_package_version_to_tracker' );
43+
44+
$this->assertSame( $expected, apply_filters( 'jetpack_package_versions', $existing_array ) );
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2+
3+
namespace Automattic\Jetpack\Stats;
4+
5+
use Automattic\Jetpack\Connection\Rest_Authentication as Connection_Rest_Authentication;
6+
use PHPUnit\Framework\TestCase;
7+
use WP_REST_Request;
8+
use WP_REST_Server;
9+
10+
/**
11+
* Unit tests for the REST API endpoints.
12+
*
13+
* @package automattic/jetpack-stats
14+
* @see \Automattic\Jetpack\Stats\REST_Provider
15+
*/
16+
class Test_REST_Provider extends TestCase {
17+
/**
18+
* REST Server object.
19+
*
20+
* @var WP_REST_Server
21+
*/
22+
private $server;
23+
24+
const BLOG_TOKEN = 'new.blogtoken';
25+
const BLOG_ID = 42;
26+
27+
/**
28+
* Setting up the test.
29+
*
30+
* @before
31+
*/
32+
public function set_up() {
33+
global $wp_rest_server;
34+
35+
$wp_rest_server = new WP_REST_Server();
36+
$this->server = $wp_rest_server;
37+
38+
REST_Provider::init( true );
39+
do_action( 'rest_api_init' );
40+
}
41+
42+
/**
43+
* Returning the environment into its initial state.
44+
*
45+
* @after
46+
*/
47+
public function tear_down() {
48+
unset( $_SERVER['REQUEST_METHOD'] );
49+
$_GET = array();
50+
51+
Connection_Rest_Authentication::init()->reset_saved_auth_state();
52+
}
53+
54+
/**
55+
* Testing the `remote_provision` endpoint without authentication.
56+
* Response: failed authorization.
57+
*/
58+
public function test_get_blog_unauthenticated() {
59+
wp_set_current_user( 0 );
60+
$request = new WP_REST_Request( 'GET', '/jetpack/v4/stats/blog' );
61+
$request->set_header( 'Content-Type', 'application/json' );
62+
63+
// Mock full connection established.
64+
add_filter( 'jetpack_options', array( $this, 'mock_jetpack_options' ), 10, 2 );
65+
66+
$response = $this->server->dispatch( $request );
67+
$response_data = $response->get_data();
68+
69+
remove_filter( 'jetpack_options', array( $this, 'mock_jetpack_options' ), 10 );
70+
71+
static::assertEquals( 'invalid_permission_stats_get_blog', $response_data['code'] );
72+
static::assertEquals( 401, $response_data['data']['status'] );
73+
}
74+
75+
/**
76+
* Testing the `remote_provision` endpoint with proper authentication.
77+
* We intentionally provide an invalid user ID so the `Jetpack_XMLRPC_Server::remote_provision()` would trigger an error.
78+
* Response: `input_error`, meaning that the REST endpoint passed the data to the handler.
79+
*/
80+
public function test_get_blog_authenticated() {
81+
wp_set_current_user( 0 );
82+
83+
// Mock full connection established.
84+
add_filter( 'jetpack_options', array( $this, 'mock_jetpack_options' ), 10, 2 );
85+
86+
$_SERVER['REQUEST_METHOD'] = 'POST';
87+
88+
$_GET['_for'] = 'jetpack';
89+
$_GET['token'] = 'new:1:0';
90+
$_GET['timestamp'] = (string) time();
91+
$_GET['nonce'] = 'testing123';
92+
$_GET['body-hash'] = '';
93+
// This is intentionally using base64_encode().
94+
// phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
95+
// phpcs:disable WordPress.Security.NonceVerification.Recommended
96+
// phpcs:disable WordPress.Security.ValidatedSanitizedInput
97+
$_GET['signature'] = base64_encode(
98+
hash_hmac(
99+
'sha1',
100+
implode(
101+
"\n",
102+
$data = array(
103+
$_GET['token'],
104+
$_GET['timestamp'],
105+
$_GET['nonce'],
106+
$_GET['body-hash'],
107+
'POST',
108+
'anything.example',
109+
'80',
110+
'',
111+
)
112+
) . "\n",
113+
'blogtoken',
114+
true
115+
)
116+
);
117+
// phpcs:enable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
118+
// phpcs:enable WordPress.Security.NonceVerification.Recommended
119+
// phpcs:enable WordPress.Security.ValidatedSanitizedInput
120+
121+
Connection_Rest_Authentication::init()->wp_rest_authenticate( false );
122+
123+
$request = new WP_REST_Request( 'GET', '/jetpack/v4/stats/blog' );
124+
$request->set_header( 'Content-Type', 'application/json' );
125+
126+
$response = $this->server->dispatch( $request );
127+
$response_data = $response->get_data();
128+
129+
remove_filter( 'jetpack_options', array( $this, 'mock_jetpack_options' ), 10 );
130+
131+
$expected_stats_blog = array(
132+
'admin_bar' => true,
133+
'count_roles' => array(),
134+
'do_not_track' => true,
135+
'version' => Main::STATS_VERSION,
136+
'collapse_nudges' => false,
137+
'enable_odyssey_stats' => true,
138+
'odyssey_stats_changed_at' => 0,
139+
'notices' => array(),
140+
'views' => 0,
141+
'host' => 'example.org',
142+
'path' => '/',
143+
'blogname' => false,
144+
'blogdescription' => false,
145+
'siteurl' => 'http://example.org',
146+
'gmt_offset' => false,
147+
'timezone_string' => false,
148+
'stats_version' => Main::STATS_VERSION,
149+
'stats_api' => 'jetpack',
150+
'page_on_front' => false,
151+
'permalink_structure' => false,
152+
'category_base' => false,
153+
'tag_base' => false,
154+
);
155+
156+
$this->assertSame( $expected_stats_blog, $response_data );
157+
}
158+
159+
/**
160+
* Intercept the `Jetpack_Options` call and mock the values.
161+
* Full connection set-up.
162+
*
163+
* @param mixed $value The current option value.
164+
* @param string $name Option name.
165+
*
166+
* @return mixed
167+
*/
168+
public function mock_jetpack_options( $value, $name ) {
169+
switch ( $name ) {
170+
case 'blog_token':
171+
return self::BLOG_TOKEN;
172+
case 'id':
173+
return self::BLOG_ID;
174+
}
175+
176+
return $value;
177+
}
178+
}

projects/packages/stats/tests/php/test-xmlrpc-provider.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class Test_XMLRPC_Provider extends StatsBaseTestCase {
3030
protected function set_up() {
3131
parent::set_up();
3232

33-
$this->xmlrpc_instance = XMLRPC_Provider::init();
33+
$this->xmlrpc_instance = XMLRPC_Provider::init( true );
3434
}
3535

3636
/**

0 commit comments

Comments
 (0)