Completed
Push — add/sal-post ( 4a4572 )
by
unknown
10:34
created

WPCOM_JSON_API_Links   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 259
Duplicated Lines 36.29 %

Coupling/Cohesion

Components 2
Dependencies 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 44
c 1
b 0
f 0
lcom 2
cbo 1
dl 94
loc 259
rs 8.3396

18 Methods

Rating   Name   Duplication   Size   Complexity  
A getInstance() 0 7 2
A __construct() 0 3 1
A __clone() 0 1 1
A __wakeup() 0 1 1
B get_link() 33 33 4
A get_me_link() 0 3 1
A get_taxonomy_link() 4 6 2
A get_media_link() 0 3 1
A get_site_link() 0 3 1
A get_post_link() 0 3 1
A get_comment_link() 0 3 1
A get_publicize_connection_link() 0 3 1
A get_publicize_connections_link() 0 3 1
A get_keyring_connection_link() 0 3 1
A get_external_service_link() 0 3 1
D get_closest_version_of_endpoint() 14 81 19
B get_endpoint_path_versions() 34 34 3
A get_last_segment_of_relative_path() 9 9 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WPCOM_JSON_API_Links often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WPCOM_JSON_API_Links, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
require_once( ABSPATH . '/public.api/rest/class.json-api.php' );
4
5
class WPCOM_JSON_API_Links {
6
	private $api;
7
	private static $instance;
8
9
	public static function getInstance() {
10
		if (null === static::$instance) {
0 ignored issues
show
Bug introduced by
Since $instance is declared private, accessing it with static will lead to errors in possible sub-classes; consider using self, or increasing the visibility of $instance to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return static::$someVariable;
    }
}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass { }

YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class SomeClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return self::$someVariable; // self works fine with private.
    }
}
Loading history...
11
			static::$instance = new static();
0 ignored issues
show
Bug introduced by
Since $instance is declared private, accessing it with static will lead to errors in possible sub-classes; consider using self, or increasing the visibility of $instance to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return static::$someVariable;
    }
}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass { }

YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class SomeClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return self::$someVariable; // self works fine with private.
    }
}
Loading history...
12
		}
13
		
14
		return static::$instance;
0 ignored issues
show
Bug introduced by
Since $instance is declared private, accessing it with static will lead to errors in possible sub-classes; consider using self, or increasing the visibility of $instance to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return static::$someVariable;
    }
}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass { }

YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class SomeClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return self::$someVariable; // self works fine with private.
    }
}
Loading history...
15
	}
16
17
	// protect these methods for singleton
18
	protected function __construct() { 
19
		$this->api = WPCOM_JSON_API::init();
20
	}
21
	private function __clone() { }
22
	private function __wakeup() { }
23
24
	/**
25
	 * Generate a URL to an endpoint
26
	 *
27
	 * Used to construct meta links in API responses
28
	 *
29
	 * @param mixed $args Optional arguments to be appended to URL
0 ignored issues
show
Bug introduced by
There is no parameter named $args. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
30
	 * @return string Endpoint URL
31
	 **/
32 View Code Duplication
	function get_link() {
33
		$args   = func_get_args();
34
		$format = array_shift( $args );
35
		$base = WPCOM_JSON_API__BASE;
36
37
		$path = array_pop( $args );
38
39
		if ( $path ) {
40
			$path = '/' . ltrim( $path, '/' );
41
		}
42
43
		$args[] = $path;
44
45
		// Escape any % in args before using sprintf
46
		$escaped_args = array();
47
		foreach ( $args as $arg_key => $arg_value ) {
48
			$escaped_args[ $arg_key ] = str_replace( '%', '%%', $arg_value );
49
		}
50
51
		$relative_path = vsprintf( "$format%s", $escaped_args );
52
53
		if ( ! wp_startswith( $relative_path, '.' ) ) {
54
			// Generic version. Match the requested version as best we can
55
			$api_version = $this->get_closest_version_of_endpoint( $format, $relative_path );
56
			$base        = substr( $base, 0, - 1 ) . $api_version;
57
		}
58
59
		// escape any % in the relative path before running it through sprintf again
60
		$relative_path = str_replace( '%', '%%', $relative_path );
61
		// http, WPCOM_JSON_API__BASE, ...    , path
62
		// %s  , %s                  , $format, %s
63
		return esc_url_raw( sprintf( "https://%s$relative_path", $base ) );
64
	}
65
66
	function get_me_link( $path = '' ) {
67
		return $this->get_link( '/me', $path );
68
	}
69
70
	function get_taxonomy_link( $blog_id, $taxonomy_id, $taxonomy_type, $path = '' ) {
71 View Code Duplication
		if ( 'category' === $taxonomy_type )
72
			return $this->get_link( '/sites/%d/categories/slug:%s', $blog_id, $taxonomy_id, $path );
73
		else
74
			return $this->get_link( '/sites/%d/tags/slug:%s', $blog_id, $taxonomy_id, $path );
75
	}
76
77
	function get_media_link( $blog_id, $media_id, $path = '' ) {
78
		return $this->get_link( '/sites/%d/media/%d', $blog_id, $media_id, $path );
79
	}
80
81
	function get_site_link( $blog_id, $path = '' ) {
82
		return $this->get_link( '/sites/%d', $blog_id, $path );
83
	}
84
85
	function get_post_link( $blog_id, $post_id, $path = '' ) {
86
		return $this->get_link( '/sites/%d/posts/%d', $blog_id, $post_id, $path );
87
	}
88
89
	function get_comment_link( $blog_id, $comment_id, $path = '' ) {
90
		return $this->get_link( '/sites/%d/comments/%d', $blog_id, $comment_id, $path );
91
	}
92
93
	function get_publicize_connection_link( $blog_id, $publicize_connection_id, $path = '' ) {
94
		return $this->get_link( '.1/sites/%d/publicize-connections/%d', $blog_id, $publicize_connection_id, $path );
95
	}
96
97
	function get_publicize_connections_link( $keyring_token_id, $path = '' ) {
98
		return $this->get_link( '.1/me/publicize-connections/?keyring_connection_ID=%d', $keyring_token_id, $path );
99
	}
100
101
	function get_keyring_connection_link( $keyring_token_id, $path = '' ) {
102
		return $this->get_link( '.1/me/keyring-connections/%d', $keyring_token_id, $path );
103
	}
104
105
	function get_external_service_link( $external_service, $path = '' ) {
106
		return $this->get_link( '.1/meta/external-services/%s', $external_service, $path );
107
	}
108
109
	/**
110
	 * Try to find the closest supported version of an endpoint to the current endpoint
111
	 *
112
	 * For example, if we were looking at the path /animals/panda:
113
	 * - if the current endpoint is v1.3 and there is a v1.3 of /animals/%s available, we return 1.3
114
	 * - if the current endpoint is v1.3 and there is no v1.3 of /animals/%s known, we fall back to the
115
	 *   maximum available version of /animals/%s, e.g. 1.1
116
	 *
117
	 * This method is used in get_link() to construct meta links for API responses.
118
	 * 
119
	 * @param $template_path The generic endpoint path, e.g. /sites/%s
120
	 * @param $path string The current endpoint path, relative to the version, e.g. /sites/12345
121
	 * @param $method string Request method used to access the endpoint path
122
	 * @return string The current version, or otherwise the maximum version available
123
	 */
124
	function get_closest_version_of_endpoint( $template_path, $path, $request_method = 'GET' ) {
125
		static $closest_endpoint_cache;
126
127
		if ( ! $closest_endpoint_cache ) {
128
			$closest_endpoint_cache = array();
129
		}
130
131
		if ( ! isset( $closest_endpoint_cache[ $template_path ] ) ) {
132
			$closest_endpoint_cache[ $template_path ] = array();
133
		} elseif ( isset( $closest_endpoint_cache[ $template_path ][ $request_method ] ) ) {
134
			return $closest_endpoint_cache[ $template_path ][ $request_method ];	
135
		}
136
137
		$path = untrailingslashit( $path );
138
139
		// /help is a special case - always use the current request version
140
		if ( wp_endswith( $path, '/help' ) ) {
141
			return $closest_endpoint_cache[ $template_path ][ $request_method ] = $this->api->version;
142
		}
143
144
		static $matches;
145 View Code Duplication
		if ( empty( $matches ) ) {
146
			$matches = array();
147
		} else {
148
			// try to match out of saved matches
149
			foreach( $matches as $match ) {
150
				$regex = $match->regex;
151
				if ( preg_match( "#^$regex\$#", $path ) ) {
152
					return $closest_endpoint_cache[ $template_path ][ $request_method ] = $match->version;
153
				}
154
			}
155
		}
156
157
		$endpoint_path_versions = $this->get_endpoint_path_versions();
158
		$last_path_segment = $this->get_last_segment_of_relative_path( $path );
159
		$max_version_found = null;
160
161
		foreach ( $endpoint_path_versions as $endpoint_last_path_segment => $endpoints ) {
162
163
			// Does the last part of the path match the path key? (e.g. 'posts')
164
			// If the last part contains a placeholder (e.g. %s), we want to carry on
165
			if ( $last_path_segment != $endpoint_last_path_segment && ! strstr( $endpoint_last_path_segment, '%' ) ) {
166
				continue;
167
			}
168
169
			foreach ( $endpoints as $endpoint ) {
170
				// Does the request method match?
171
				if ( ! in_array( $request_method, $endpoint['request_methods'] ) ) {
172
					continue;
173
				}
174
175
				$endpoint_path = untrailingslashit( $endpoint['path'] );
176
				$endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path );
177
178
				if ( ! preg_match( "#^$endpoint_path_regex\$#", $path, $matches ) ) {
179
					continue;
180
				}
181
182
				// Make sure the endpoint exists at the same version
183
				if ( version_compare( $this->api->version, $endpoint['min_version'], '>=') &&
184
					 version_compare( $this->api->version, $endpoint['max_version'], '<=') ) {
185
					array_push( $matches, (object) array( 'version' => $this->api->version, 'regex' => $endpoint_path_regex ) );
186
					return $closest_endpoint_cache[ $template_path ][ $request_method ] = $this->api->version;
187
				}
188
189
				// If the endpoint doesn't exist at the same version, record the max version we found
190 View Code Duplication
				if ( empty( $max_version_found ) || version_compare( $max_version_found['version'], $endpoint['max_version'], '<' ) ) {
191
					$max_version_found = array( 'version' => $endpoint['max_version'], 'regex' => $endpoint_path_regex );
192
				}
193
			}
194
		}
195
196
		// If the endpoint version is less than the requested endpoint version, return the max version found
197
		if ( ! empty( $max_version_found ) ) {
198
			array_push( $matches, (object) $max_version_found );
199
			return $max_version_found['version'];
200
		}
201
202
		// Otherwise, use the API version of the current request
203
		return $this->api->version;
204
	}
205
206
	/**
207
	 * Get an array of endpoint paths with their associated versions
208
	 *
209
	 * The result is cached for 30 minutes.
210
	 *
211
	 * @return array Array of endpoint paths, min_versions and max_versions, keyed by last segment of path
212
	 **/
213 View Code Duplication
	protected function get_endpoint_path_versions() {
214
215
		static $cache_result;
216
217
		if ( ! empty ( $cache_result ) ) {
218
			return $cache_result;
219
		}
220
221
		/*
222
		 * Create a map of endpoints and their min/max versions keyed by the last segment of the path (e.g. 'posts')
223
		 * This reduces the search space when finding endpoint matches in get_closest_version_of_endpoint()
224
		 */
225
		$endpoint_path_versions = array();
226
227
		foreach ( $this->api->endpoints as $key => $endpoint_objects ) {
228
229
			// The key contains a serialized path, min_version and max_version
230
			list( $path, $min_version, $max_version ) = unserialize( $key );
231
232
			// Grab the last component of the relative path to use as the top-level key
233
			$last_path_segment = $this->get_last_segment_of_relative_path( $path );
234
235
			$endpoint_path_versions[ $last_path_segment ][] = array(
236
				'path' => $path,
237
				'min_version' => $min_version,
238
				'max_version' => $max_version,
239
				'request_methods' => array_keys( $endpoint_objects )
240
			);
241
		}
242
243
		$cache_result = $endpoint_path_versions;
244
245
		return $endpoint_path_versions;
246
	}
247
248
	/**
249
	 * Grab the last segment of a relative path
250
	 *
251
	 * @param string $path Path
252
	 * @return string Last path segment
253
	 */
254 View Code Duplication
	protected function get_last_segment_of_relative_path( $path) {
255
		$path_parts = array_filter( explode( '/', $path ) );
256
257
		if ( empty( $path_parts ) ) {
258
			return null;
259
		}
260
261
		return end( $path_parts );
262
	}
263
}