summaryrefslogtreecommitdiff
blob: 6746f202c9c5656f0a234097e84bee48f1529bcb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
<?php

require_once dirname( __FILE__ ) . '/../class.json-api.php';

class WPCOM_JSON_API_Links {
	private $api;
	private static $instance;
	private $closest_endpoint_cache_by_version = array();
	private $matches_by_version = array();
	private $cache_result = null;

	public static function getInstance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	// protect these methods for singleton
	protected function __construct() { 
		$this->api = WPCOM_JSON_API::init();
	}
	private function __clone() { }
	private function __wakeup() { }

	/**
	 * Generate a URL to an endpoint
	 *
	 * Used to construct meta links in API responses
	 *
	 * @param mixed $args Optional arguments to be appended to URL
	 * @return string Endpoint URL
	 **/
	function get_link() {
		$args   = func_get_args();
		$format = array_shift( $args );
		$base = WPCOM_JSON_API__BASE;

		$path = array_pop( $args );

		if ( $path ) {
			$path = '/' . ltrim( $path, '/' );
			// tack the path onto the end of the format string
			// have to escape %'s in the path as %% because
			// we're about to pass it through sprintf and we don't
			// want it to see the % as a placeholder
			$format .= str_replace( '%', '%%', $path );
		}

		// Escape any % in args before using sprintf
		$escaped_args = array();
		foreach ( $args as $arg_key => $arg_value ) {
			$escaped_args[ $arg_key ] = str_replace( '%', '%%', $arg_value );
		}

		$relative_path = vsprintf( $format, $escaped_args );

		if ( ! wp_startswith( $relative_path, '.' ) ) {
			// Generic version. Match the requested version as best we can
			$api_version = $this->get_closest_version_of_endpoint( $format, $relative_path );
			$base        = substr( $base, 0, - 1 ) . $api_version;
		}

		// escape any % in the relative path before running it through sprintf again
		$relative_path = str_replace( '%', '%%', $relative_path );
		// http, WPCOM_JSON_API__BASE, ...    , path
		// %s  , %s                  , $format, %s
		return esc_url_raw( sprintf( "https://%s$relative_path", $base ) );
	}

	function get_me_link( $path = '' ) {
		return $this->get_link( '/me', $path );
	}

	function get_taxonomy_link( $blog_id, $taxonomy_id, $taxonomy_type, $path = '' ) {
		switch ( $taxonomy_type ) {
			case 'category':
				return $this->get_link( '/sites/%d/categories/slug:%s', $blog_id, $taxonomy_id, $path );

			case 'post_tag':
				return $this->get_link( '/sites/%d/tags/slug:%s', $blog_id, $taxonomy_id, $path );

			default:
				return $this->get_link( '/sites/%d/taxonomies/%s/terms/slug:%s', $blog_id, $taxonomy_type, $taxonomy_id, $path );
		}
	}

	function get_media_link( $blog_id, $media_id, $path = '' ) {
		return $this->get_link( '/sites/%d/media/%d', $blog_id, $media_id, $path );
	}

	function get_site_link( $blog_id, $path = '' ) {
		return $this->get_link( '/sites/%d', $blog_id, $path );
	}

	function get_post_link( $blog_id, $post_id, $path = '' ) {
		return $this->get_link( '/sites/%d/posts/%d', $blog_id, $post_id, $path );
	}

	function get_comment_link( $blog_id, $comment_id, $path = '' ) {
		return $this->get_link( '/sites/%d/comments/%d', $blog_id, $comment_id, $path );
	}

	function get_publicize_connection_link( $blog_id, $publicize_connection_id, $path = '' ) {
		return $this->get_link( '.1/sites/%d/publicize-connections/%d', $blog_id, $publicize_connection_id, $path );
	}

	function get_publicize_connections_link( $keyring_token_id, $path = '' ) {
		return $this->get_link( '.1/me/publicize-connections/?keyring_connection_ID=%d', $keyring_token_id, $path );
	}

	function get_keyring_connection_link( $keyring_token_id, $path = '' ) {
		return $this->get_link( '.1/me/keyring-connections/%d', $keyring_token_id, $path );
	}

	function get_external_service_link( $external_service, $path = '' ) {
		return $this->get_link( '.1/meta/external-services/%s', $external_service, $path );
	}

	/**
	 * Try to find the closest supported version of an endpoint to the current endpoint
	 *
	 * For example, if we were looking at the path /animals/panda:
	 * - if the current endpoint is v1.3 and there is a v1.3 of /animals/%s available, we return 1.3
	 * - if the current endpoint is v1.3 and there is no v1.3 of /animals/%s known, we fall back to the
	 *   maximum available version of /animals/%s, e.g. 1.1
	 *
	 * This method is used in get_link() to construct meta links for API responses.
	 * 
	 * @param $template_path string The generic endpoint path, e.g. /sites/%s
	 * @param $path string The current endpoint path, relative to the version, e.g. /sites/12345
	 * @param $request_method string Request method used to access the endpoint path
	 * @return string The current version, or otherwise the maximum version available
	 */
	function get_closest_version_of_endpoint( $template_path, $path, $request_method = 'GET' ) {
		$closest_endpoint_cache_by_version = & $this->closest_endpoint_cache_by_version;

		$closest_endpoint_cache = & $closest_endpoint_cache_by_version[ $this->api->version ];
		if ( !$closest_endpoint_cache ) {
			$closest_endpoint_cache_by_version[ $this->api->version ] = array();
			$closest_endpoint_cache = & $closest_endpoint_cache_by_version[ $this->api->version ];
		}

		if ( ! isset( $closest_endpoint_cache[ $template_path ] ) ) {
			$closest_endpoint_cache[ $template_path ] = array();
		} elseif ( isset( $closest_endpoint_cache[ $template_path ][ $request_method ] ) ) {
			return $closest_endpoint_cache[ $template_path ][ $request_method ];
		}

		$path = untrailingslashit( $path );

		// /help is a special case - always use the current request version
		if ( wp_endswith( $path, '/help' ) ) {
			$closest_endpoint_cache[ $template_path ][ $request_method ] = $this->api->version;
			return $this->api->version;
		}

		$matches_by_version = & $this->matches_by_version;

		// try to match out of saved matches
		$matches = isset( $matches_by_version[ $this->api->version ] )
			? $matches_by_version[ $this->api->version ]
			: false;
		if ( ! $matches ) {
			$matches_by_version[ $this->api->version ] = array();
			$matches = & $matches_by_version[ $this->api->version ];
		}
		foreach ( $matches as $match ) {
			$regex = $match->regex;
			if ( preg_match( "#^$regex\$#", $path ) ) {
				$closest_endpoint_cache[ $template_path ][ $request_method ] = $match->version;
				return $match->version;
			}
		}

		$endpoint_path_versions = $this->get_endpoint_path_versions();
		$last_path_segment = $this->get_last_segment_of_relative_path( $path );
		$max_version_found = null;

		foreach ( $endpoint_path_versions as $endpoint_last_path_segment => $endpoints ) {

			// Does the last part of the path match the path key? (e.g. 'posts')
			// If the last part contains a placeholder (e.g. %s), we want to carry on
			if ( $last_path_segment != $endpoint_last_path_segment && ! strstr( $endpoint_last_path_segment, '%' ) ) {
				continue;
			}

			foreach ( $endpoints as $endpoint ) {
				// Does the request method match?
				if ( ! in_array( $request_method, $endpoint['request_methods'] ) ) {
					continue;
				}

				$endpoint_path = untrailingslashit( $endpoint['path'] );
				$endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path );

				if ( ! preg_match( "#^$endpoint_path_regex\$#", $path ) ) {
					continue;
				}

				// Make sure the endpoint exists at the same version
				if ( version_compare( $this->api->version, $endpoint['min_version'], '>=') &&
					 version_compare( $this->api->version, $endpoint['max_version'], '<=') ) {
					array_push( $matches, (object) array( 'version' => $this->api->version, 'regex' => $endpoint_path_regex ) );
					$closest_endpoint_cache[ $template_path ][ $request_method ] = $this->api->version;
					return $this->api->version;
				}

				// If the endpoint doesn't exist at the same version, record the max version we found
				if ( empty( $max_version_found ) || version_compare( $max_version_found['version'], $endpoint['max_version'], '<' ) ) {
					$max_version_found = array( 'version' => $endpoint['max_version'], 'regex' => $endpoint_path_regex );
				}
			}
		}

		// If the endpoint version is less than the requested endpoint version, return the max version found
		if ( ! empty( $max_version_found ) ) {
			array_push( $matches, (object) $max_version_found );
			$closest_endpoint_cache[ $template_path ][ $request_method ] = $max_version_found['version'];
			return $max_version_found['version'];
		}

		// Otherwise, use the API version of the current request
		return $this->api->version;
	}

	/**
	 * Get an array of endpoint paths with their associated versions
	 *
	 * @return array Array of endpoint paths, min_versions and max_versions, keyed by last segment of path
	 **/
	protected function get_endpoint_path_versions() {

		if ( ! empty ( $this->cache_result ) ) {
			return $this->cache_result;
		}

		/*
		 * Create a map of endpoints and their min/max versions keyed by the last segment of the path (e.g. 'posts')
		 * This reduces the search space when finding endpoint matches in get_closest_version_of_endpoint()
		 */
		$endpoint_path_versions = array();

		foreach ( $this->api->endpoints as $key => $endpoint_objects ) {

			// The key contains a serialized path, min_version and max_version
			list( $path, $min_version, $max_version ) = unserialize( $key );

			// Grab the last component of the relative path to use as the top-level key
			$last_path_segment = $this->get_last_segment_of_relative_path( $path );

			$endpoint_path_versions[ $last_path_segment ][] = array(
				'path' => $path,
				'min_version' => $min_version,
				'max_version' => $max_version,
				'request_methods' => array_keys( $endpoint_objects )
			);
		}

		$this->cache_result = $endpoint_path_versions;

		return $endpoint_path_versions;
	}

	/**
	 * Grab the last segment of a relative path
	 *
	 * @param string $path Path
	 * @return string Last path segment
	 */
	protected function get_last_segment_of_relative_path( $path) {
		$path_parts = array_filter( explode( '/', $path ) );

		if ( empty( $path_parts ) ) {
			return null;
		}

		return end( $path_parts );
	}
}