Completed
Push — master-stable ( 4a8b26...01e321 )
by
unknown
09:43
created

class.wpcom-json-api-get-site-endpoint.php (8 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
class WPCOM_JSON_API_GET_Site_Endpoint extends WPCOM_JSON_API_Endpoint {
4
5
	public static $site_format = array(
6
		'ID'                => '(int) Site ID',
7
		'name'              => '(string) Title of site',
8
		'description'       => '(string) Tagline or description of site',
9
		'URL'               => '(string) Full URL to the site',
10
		'user_can_manage'   => '(bool) The current user can manage this site', // deprecated
11
		'capabilities'      => '(array) Array of capabilities for the current user on this site.',
12
		'jetpack'           => '(bool)  Whether the site is a Jetpack site or not',
13
		'is_multisite'      => '(bool) Whether the site is a Multisite site or not. Always true for WP.com sites.',
14
		'post_count'        => '(int) The number of posts the site has',
15
		'subscribers_count' => '(int) The number of subscribers the site has',
16
		'lang'              => '(string) Primary language code of the site',
17
		'icon'              => '(array) An array of icon formats for the site',
18
		'logo'              => '(array) The site logo, set in the Customizer',
19
		'visible'           => '(bool) If this site is visible in the user\'s site list',
20
		'is_private'        => '(bool) If the site is a private site or not',
21
		'single_user_site'  => '(bool) Whether the site is single user. Only returned for WP.com sites and for Jetpack sites with version 3.4 or higher.',
22
		'is_vip'            => '(bool) If the site is a VIP site or not.',
23
		'is_following'      => '(bool) If the current user is subscribed to this site in the reader',
24
		'options'           => '(array) An array of options/settings for the blog. Only viewable by users with post editing rights to the site. Note: Post formats is deprecated, please see /sites/$id/post-formats/',
25
		'plan'              => '(array) Details of the current plan for this site.',
26
		'updates'           => '(array) An array of available updates for plugins, themes, wordpress, and languages.',
27
		'jetpack_modules'   => '(array) A list of active Jetpack modules.',
28
		'meta'              => '(object) Meta data',
29
	);
30
31
	protected static $site_options_format = array(
32
		'timezone',
33
		'gmt_offset',
34
		'videopress_enabled',
35
		'upgraded_filetypes_enabled',
36
		'login_url',
37
		'admin_url',
38
		'is_mapped_domain',
39
		'is_redirect',
40
		'unmapped_url',
41
		'featured_images_enabled',
42
		'theme_slug',
43
		'header_image',
44
		'background_color',
45
		'image_default_link_type',
46
		'image_thumbnail_width',
47
		'image_thumbnail_height',
48
		'image_thumbnail_crop',
49
		'image_medium_width',
50
		'image_medium_height',
51
		'image_large_width',
52
		'image_large_height',
53
		'permalink_structure',
54
		'post_formats',
55
		'default_post_format',
56
		'default_category',
57
		'allowed_file_types',
58
		'show_on_front',
59
		/** This filter is documented in modules/likes.php */
60
		'default_likes_enabled',
61
		'default_sharing_status',
62
		'default_comment_status',
63
		'default_ping_status',
64
		'software_version',
65
		'created_at',
66
		'wordads',
67
		'publicize_permanently_disabled',
68
		'frame_nonce',
69
		'page_on_front',
70
		'page_for_posts',
71
		'ak_vp_bundle_enabled'
72
	);
73
74
	protected static $jetpack_response_field_additions = array(
75
		'capabilities',
76
		'plan',
77
		'subscribers_count'
78
	);
79
80
	protected static $jetpack_response_option_additions = array(
81
		'publicize_permanently_disabled',
82
		'ak_vp_bundle_enabled'
83
	);
84
85
	private $site;
86
	// protected $compact = null;
87
	protected $fields_to_include = '_all';
88
	protected $options_to_include = '_all';
89
90
	// /sites/mine
91
	// /sites/%s -> $blog_id
92
	function callback( $path = '', $blog_id = 0 ) {
93
		if ( 'mine' === $blog_id ) {
94
			$api = WPCOM_JSON_API::init();
95
			if ( ! $api->token_details || empty( $api->token_details['blog_id'] ) ) {
96
				return new WP_Error( 'authorization_required', 'An active access token must be used to query information about the current blog.', 403 );
97
			}
98
			$blog_id = $api->token_details['blog_id'];
99
		}
100
101
		$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
102
		if ( is_wp_error( $blog_id ) ) {
103
			return $blog_id;
104
		}
105
106
		$response = $this->build_current_site_response();
107
108
		/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
109
		do_action( 'wpcom_json_api_objects', 'sites' );
110
111
		return $response;
112
	}
113
114
	public function filter_fields_and_options() {
115
		$query_args = $this->query_args();
116
117
		$this->fields_to_include  = empty( $query_args['fields'] ) ? '_all' : array_map( 'trim', explode( ',', $query_args['fields'] ) );
0 ignored issues
show
Documentation Bug introduced by
It seems like empty($query_args['field...$query_args['fields'])) can also be of type array. However, the property $fields_to_include is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
118
		$this->options_to_include = empty( $query_args['options'] ) ? '_all' : array_map( 'trim', explode( ',', $query_args['options'] ) );
0 ignored issues
show
Documentation Bug introduced by
It seems like empty($query_args['optio...query_args['options'])) can also be of type array. However, the property $options_to_include is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
119
	}
120
121
	protected function include_response_field( $field ) {
122
		if ( is_array( $this->fields_to_include ) ) {
123
			return in_array( $field, $this->fields_to_include );
124
		}
125
		return true;
126
	}
127
128
	/**
129
	 * Collects the necessary information to return for a site's response.
130
	 *
131
	 * @return (array)
132
	 */
133
	public function build_current_site_response() {
134
		$blog_id = (int) $this->api->get_blog_id_for_output();
135
136
		$this->site = wpcom_get_sal_site( $blog_id );
137
138
		// Allow update in later versions
139
		/**
140
		 * Filter the structure of information about the site to return.
141
		 *
142
		 * @module json-api
143
		 *
144
		 * @since 3.9.3
145
		 *
146
		 * @param array $site_format Data structure.
147
		 */
148
		$response_format = apply_filters( 'sites_site_format', self::$site_format );
0 ignored issues
show
$response_format is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
149
		$default_fields = array_keys( apply_filters( 'sites_site_format', self::$site_format ) );
150
151
		$response_keys = is_array( $this->fields_to_include ) ?
152
			array_intersect( $default_fields, $this->fields_to_include ) :
153
			$default_fields;
154
155
		return $this->render_response_keys( $response_keys );
156
	}
157
158
	private function render_response_keys( &$response_keys ) {
159
		$response = array();
160
161
		$is_user_logged_in = is_user_logged_in();
162
163
		$this->site->before_render();
164
165
		foreach ( $response_keys as $key ) {
166
			$this->render_response_key( $key, $response, $is_user_logged_in );
167
		}
168
169
		$this->site->after_render( $response );
170
171
		return $response;
172
	}
173
174
	protected function render_response_key( $key, &$response, $is_user_logged_in ) {
175
		do_action( 'pre_render_site_response_key', $key );
176
177
		switch ( $key ) {
178
			case 'ID' :
179
				$response[ $key ] = $this->site->blog_id;
180
				break;
181
			case 'name' :
182
				$response[ $key ] = (string) htmlspecialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES );
183
				break;
184
			case 'description' :
185
				$response[ $key ] = (string) htmlspecialchars_decode( get_bloginfo( 'description' ), ENT_QUOTES );
186
				break;
187
			case 'URL' :
188
				$response[ $key ] = (string) home_url();
189
				break;
190
			case 'user_can_manage' :
0 ignored issues
show
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
191
				$response[ $key ] = $this->site->user_can_manage();
0 ignored issues
show
Are you sure the assignment to $response[$key] is correct as $this->site->user_can_manage() (which targets SAL_Site::user_can_manage()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
192
			case 'is_private' :
193
				$response[ $key ] = $this->site->is_private();
194
				break;
195
			case 'visible' :
196
				$response[ $key ] = $this->site->is_visible();
197
				break;
198
			case 'subscribers_count' :
199
				$response[ $key ] = $this->site->get_subscribers_count();
200
				break;
201
			case 'post_count' :
202
				if ( $is_user_logged_in ) {
203
					$response[ $key ] = (int) wp_count_posts( 'post' )->publish;
204
				}
205
				break;
206
			case 'icon' :
207
				$icon = $this->site->get_icon();
208
209
				if ( ! is_null( $icon ) ) {
210
					$response[ $key ] = $icon;
211
				}
212
				break;
213
			case 'logo' :
214
				$response[ $key ] = $this->site->get_logo();
215
				break;
216
			case 'is_following':
217
				$response[ $key ] = $this->site->is_following();
218
				break;
219
			case 'options':
220
				// small optimisation - don't recalculate
221
				$all_options = apply_filters( 'sites_site_options_format', self::$site_options_format );
222
223
				$options_response_keys = is_array( $this->options_to_include ) ?
224
					array_intersect( $all_options, $this->options_to_include ) :
225
					$all_options;
226
227
				$options = $this->render_option_keys( $options_response_keys );
228
229
				$this->site->after_render_options( $options );
230
231
				$response[ $key ] = $options;
232
				break;
233
			case 'meta':
234
				$this->build_meta_response( $response );
235
				break;
236
			case 'lang' :
237
				$response[ $key ] = $is_user_logged_in ? $this->site->get_locale() : false;
238
				break;
239
			case 'locale' :
240
				$response[ $key ] = $is_user_logged_in ? $this->site->get_locale() : false;
241
				break;
242
			case 'jetpack' :
243
				$response[ $key ] = $this->site->is_jetpack();
244
				break;
245
			case 'single_user_site' :
246
				$response[ $key ] = $this->site->is_single_user_site();
247
				break;
248
			case 'is_vip' :
249
				$response[ $key ] = $this->site->is_vip();
250
				break;
251
			case 'is_multisite' :
252
				$response[ $key ] = $this->site->is_multisite();
253
				break;
254
			case 'capabilities' :
255
				$response[ $key ] = $this->site->get_capabilities();
256
				break;
257
			case 'jetpack_modules':
258
				$jetpack_modules = $this->site->get_jetpack_modules();
259
				if ( ! is_null( $jetpack_modules ) ) {
260
					$response[ $key ] = $jetpack_modules;
261
				}
262
				break;
263
			case 'plan' :
264
				$response[ $key ] = $this->site->get_plan();
265
				break;
266
		}
267
268
		do_action( 'post_render_site_response_key', $key );
269
	}
270
271
	protected function render_option_keys( &$options_response_keys ) {
272
		if ( ! current_user_can( 'edit_posts' ) ) {
273
			return;
274
		}
275
276
		global $wp_version;
277
278
		$options = array();
279
280
		$custom_front_page = ( 'page' === get_option( 'show_on_front' ) );
281
282
		foreach ( $options_response_keys as $key ) {
283
			switch ( $key ) {
284
				case 'timezone' :
285
					$options[ $key ] = (string) get_option( 'timezone_string' );
286
					break;
287
				case 'gmt_offset' :
288
					$options[ $key ] = (float) get_option( 'gmt_offset' );
289
					break;
290
				case 'videopress_enabled' :
291
					$options[ $key ] = $this->site->has_videopress();
292
					break;
293
				case 'upgraded_filetypes_enabled' :
294
					$options[ $key ] = $this->site->upgraded_filetypes_enabled();
295
					break;
296
				case 'login_url' :
297
					$options[ $key ] = wp_login_url();
298
					break;
299
				case 'admin_url' :
300
					$options[ $key ] = get_admin_url();
301
					break;
302
				case 'is_mapped_domain' :
303
					$options[ $key ] = $this->site->is_mapped_domain();
304
					break;
305
				case 'is_redirect' :
306
					$options[ $key ] = $this->site->is_redirect();
307
					break;
308
				case 'unmapped_url' :
309
					$options[ $key ] = get_site_url( $this->site->blog_id );
310
					break;
311
				case 'featured_images_enabled' :
312
					$options[ $key ] = $this->site->featured_images_enabled();
313
					break;
314
				case 'theme_slug' :
315
					$options[ $key ] = get_option( 'stylesheet' );
316
					break;
317
				case 'header_image' :
318
					$options[ $key ] = get_theme_mod( 'header_image_data' );
319
					break;
320
				case 'background_color' :
321
					$options[ $key ] = get_theme_mod( 'background_color' );
322
					break;
323
				case 'image_default_link_type' :
324
					$options[ $key ] = get_option( 'image_default_link_type' );
325
					break;
326
				case 'image_thumbnail_width' :
327
					$options[ $key ] = (int) get_option( 'thumbnail_size_w' );
328
					break;
329
				case 'image_thumbnail_height' :
330
					$options[ $key ] = (int) get_option( 'thumbnail_size_h' );
331
					break;
332
				case 'image_thumbnail_crop' :
333
					$options[ $key ] = get_option( 'thumbnail_crop' );
334
					break;
335
				case 'image_medium_width' :
336
					$options[ $key ] = (int) get_option( 'medium_size_w' );
337
					break;
338
				case 'image_medium_height' :
339
					$options[ $key ] = (int) get_option( 'medium_size_h' );
340
					break;
341
				case 'image_large_width' :
342
					$options[ $key ] = (int) get_option( 'large_size_w' );
343
					break;
344
				case 'image_large_height' :
345
					$options[ $key ] = (int) get_option( 'large_size_h' );
346
					break;
347
				case 'permalink_structure' :
348
					$options[ $key ] = get_option( 'permalink_structure' );
349
					break;
350
				case 'post_formats' :
351
					$options[ $key ] = $this->site->get_post_formats();
352
					break;
353
				case 'default_post_format' :
354
					$options[ $key ] = get_option( 'default_post_format' );
355
					break;
356
				case 'default_category' :
357
					$options[ $key ] = (int) get_option( 'default_category' );
358
					break;
359
				case 'allowed_file_types' :
360
					$options[ $key ] = $this->site->allowed_file_types();
361
					break;
362
				case 'show_on_front' :
363
					$options[ $key ] = get_option( 'show_on_front' );
364
					break;
365
				/** This filter is documented in modules/likes.php */
366
				case 'default_likes_enabled' :
367
					$options[ $key ] = (bool) apply_filters( 'wpl_is_enabled_sitewide', ! get_option( 'disabled_likes' ) );
368
					break;
369
				case 'default_sharing_status' :
370
					$default_sharing_status = false;
371
					if ( class_exists( 'Sharing_Service' ) ) {
372
						$ss                     = new Sharing_Service();
373
						$blog_services          = $ss->get_blog_services();
374
						$default_sharing_status = ! empty( $blog_services['visible'] );
375
					}
376
					$options[ $key ] = (bool) $default_sharing_status;
377
					break;
378
				case 'default_comment_status' :
379
					$options[ $key ] = 'closed' !== get_option( 'default_comment_status' );
380
					break;
381
				case 'default_ping_status' :
382
					$options[ $key ] = 'closed' !== get_option( 'default_ping_status' );
383
					break;
384
				case 'software_version' :
385
					$options[ $key ] = $wp_version;
386
					break;
387
				case 'created_at' :
388
					$options[ $key ] = $this->site->get_registered_date();
389
					break;
390
				case 'wordads' :
391
					$options[ $key ] = $this->site->has_wordads();
392
					break;
393
				case 'publicize_permanently_disabled' :
394
					$publicize_permanently_disabled = false;
395
					if ( function_exists( 'is_publicize_permanently_disabled' ) ) {
396
						$publicize_permanently_disabled = is_publicize_permanently_disabled( $this->site->blog_id );
397
					}
398
					$options[ $key ] = $publicize_permanently_disabled;
399
					break;
400
				case 'frame_nonce' :
401
					$options[ $key ] = $this->site->get_frame_nonce();
402
					break;
403
				case 'page_on_front' :
404
					if ( $custom_front_page ) {
405
						$options[ $key ] = (int) get_option( 'page_on_front' );
406
					}
407
					break;
408
				case 'page_for_posts' :
409
					if ( $custom_front_page ) {
410
						$options[ $key ] = (int) get_option( 'page_for_posts' );
411
					}
412
					break;
413
				case 'ak_vp_bundle_enabled' :
414
					$options[ $key ] = $this->site->get_ak_vp_bundle_enabled();
0 ignored issues
show
Are you sure the assignment to $options[$key] is correct as $this->site->get_ak_vp_bundle_enabled() (which targets Jetpack_Site::get_ak_vp_bundle_enabled()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
415
			}
416
		}
417
418
		return $options;
419
	}
420
421
	protected function build_meta_response( &$response ) {
422
		$xmlrpc_scheme = apply_filters( 'wpcom_json_api_xmlrpc_scheme', parse_url( get_option( 'home' ), PHP_URL_SCHEME ) );
423
		$xmlrpc_url = site_url( 'xmlrpc.php', $xmlrpc_scheme );
424
		$response['meta'] = (object) array(
425
			'links' => (object) array(
426
				'self'     => (string) $this->get_site_link( $this->site->blog_id ),
427
				'help'     => (string) $this->get_site_link( $this->site->blog_id, 'help'      ),
428
				'posts'    => (string) $this->get_site_link( $this->site->blog_id, 'posts/'    ),
429
				'comments' => (string) $this->get_site_link( $this->site->blog_id, 'comments/' ),
430
				'xmlrpc'   => (string) $xmlrpc_url,
431
			),
432
		);
433
	}
434
435
	// apply any WPCOM-only response components to a Jetpack site response
436
	public function decorate_jetpack_response( &$response ) {
437
		$this->site = wpcom_get_sal_site( $blog_id );
0 ignored issues
show
The variable $blog_id does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
438
439
		// ensure the response is marked as being from Jetpack
440
		$response->jetpack = true;
441
442
		$wpcom_response = $this->render_response_keys( self::$jetpack_response_field_additions );
443
444
		foreach( $wpcom_response as $key => $value ) {
445
			$response->{ $key } = $value;
446
		}
447
448
		// render additional options
449
		if ( $response->options ) {
450
			$wpcom_options_response = $this->render_option_keys( self::$jetpack_response_option_additions );
451
452
			foreach( $wpcom_options_response as $key => $value ) {
0 ignored issues
show
The expression $wpcom_options_response of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
453
				$response->options[ $key ] = $value;
454
			}
455
			return (string) get_bloginfo( 'language' );
456
		}
457
458
		return $response; // possibly no need since it's modified in place
459
	}
460
}
461
462
class WPCOM_JSON_API_List_Post_Formats_Endpoint extends WPCOM_JSON_API_Endpoint {
463
	// /sites/%s/post-formats -> $blog_id
464
	function callback( $path = '', $blog_id = 0 ) {
465
		$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
466
		if ( is_wp_error( $blog_id ) ) {
467
			return $blog_id;
468
		}
469
470
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
471
			$this->load_theme_functions();
472
		}
473
474
		// Get a list of supported post formats.
475
		$all_formats = get_post_format_strings();
476
		$supported   = get_theme_support( 'post-formats' );
477
478
		$supported_formats = $response['formats'] = array();
479
480 View Code Duplication
		if ( isset( $supported[0] ) ) {
481
			foreach ( $supported[0] as $format ) {
482
				$supported_formats[ $format ] = $all_formats[ $format ];
483
			}
484
		}
485
486
		$response['formats'] = (object) $supported_formats;
487
488
		return $response;
489
	}
490
}
491
492
class WPCOM_JSON_API_List_Page_Templates_Endpoint extends WPCOM_JSON_API_Endpoint {
493
	// /sites/%s/page-templates -> $blog_id
494
	function callback( $path = '', $blog_id = 0 ) {
495
		$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
496
		if ( is_wp_error( $blog_id ) ) {
497
			return $blog_id;
498
		}
499
500
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
501
			$this->load_theme_functions();
502
		}
503
504
		$response = array();
505
		$page_templates = array();
506
507
		$templates = get_page_templates();
508
		ksort( $templates );
509
510
		foreach ( array_keys( $templates ) as $label ) {
511
			$page_templates[] = array(
512
				'label' => $label,
513
				'file'  => $templates[ $label ]
514
			);
515
		}
516
517
		$response['templates'] = $page_templates;
518
519
		return $response;
520
	}
521
}
522