/class.json-api-endpoints.php

  1. <?php 
  2.  
  3. require_once( dirname( __FILE__ ) . '/json-api-config.php' ); 
  4.  
  5. // Endpoint 
  6. abstract class WPCOM_JSON_API_Endpoint { 
  7. // The API Object 
  8. public $api; 
  9.  
  10. public $pass_wpcom_user_details = false; 
  11. public $can_use_user_details_instead_of_blog_membership = false; 
  12.  
  13. // One liner. 
  14. public $description; 
  15.  
  16. // Object Grouping For Documentation (Users, Posts, Comments) 
  17. public $group; 
  18.  
  19. // Stats extra value to bump 
  20. public $stat; 
  21.  
  22. // HTTP Method 
  23. public $method = 'GET'; 
  24.  
  25. // Minimum version of the api for which to serve this endpoint 
  26. public $min_version = '0'; 
  27.  
  28. // Maximum version of the api for which to serve this endpoint 
  29. public $max_version = WPCOM_JSON_API__CURRENT_VERSION; 
  30.  
  31. // Path at which to serve this endpoint: sprintf() format. 
  32. public $path = ''; 
  33.  
  34. // Identifiers to fill sprintf() formatted $path 
  35. public $path_labels = array(); 
  36.  
  37. // Accepted query parameters 
  38. public $query = array( 
  39. // Parameter name 
  40. 'context' => array( 
  41. // Default value => description 
  42. 'display' => 'Formats the output as HTML for display. Shortcodes are parsed, paragraph tags are added, etc..',  
  43. // Other possible values => description 
  44. 'edit' => 'Formats the output for editing. Shortcodes are left unparsed, significant whitespace is kept, etc..',  
  45. ),  
  46. 'http_envelope' => array( 
  47. 'false' => '',  
  48. 'true' => 'Some environments (like in-browser Javascript or Flash) block or divert responses with a non-200 HTTP status code. Setting this parameter will force the HTTP status code to always be 200. The JSON response is wrapped in an "envelope" containing the "real" HTTP status code and headers.',  
  49. ),  
  50. 'pretty' => array( 
  51. 'false' => '',  
  52. 'true' => 'Output pretty JSON',  
  53. ),  
  54. 'meta' => "(string) Optional. Loads data from the endpoints found in the 'meta' part of the response. Comma-separated list. Example: meta=site, likes",  
  55. 'fields' => '(string) Optional. Returns specified fields only. Comma-separated list. Example: fields=ID, title',  
  56. // Parameter name => description (default value is empty) 
  57. 'callback' => '(string) An optional JSONP callback function.',  
  58. ); 
  59.  
  60. // Response format 
  61. public $response_format = array(); 
  62.  
  63. // Request format 
  64. public $request_format = array(); 
  65.  
  66. // Is this endpoint still in testing phase? If so, not available to the public. 
  67. public $in_testing = false; 
  68.  
  69. // Is this endpoint still allowed if the site in question is flagged? 
  70. public $allowed_if_flagged = false; 
  71.  
  72. /** 
  73. * @var string Version of the API 
  74. */ 
  75. public $version = ''; 
  76.  
  77. /** 
  78. * @var string Example request to make 
  79. */ 
  80. public $example_request = ''; 
  81.  
  82. /** 
  83. * @var string Example request data (for POST methods) 
  84. */ 
  85. public $example_request_data = ''; 
  86.  
  87. /** 
  88. * @var string Example response from $example_request 
  89. */ 
  90. public $example_response = ''; 
  91.  
  92. /** 
  93. * @var bool Set to true if the endpoint implements its own filtering instead of the standard `fields` query method 
  94. */ 
  95. public $custom_fields_filtering = false; 
  96.  
  97. /** 
  98. * @var bool Set to true if the endpoint accepts all cross origin requests. You probably should not set this flag. 
  99. */ 
  100. public $allow_cross_origin_request = false; 
  101.  
  102. /** 
  103. * @var bool Set to true if the endpoint can recieve unauthorized POST requests. 
  104. */ 
  105. public $allow_unauthorized_request = false; 
  106.  
  107. /** 
  108. * @var bool Set to true if the endpoint should accept site based (not user based) authentication. 
  109. */ 
  110. public $allow_jetpack_site_auth = false; 
  111.  
  112. function __construct( $args ) { 
  113. $defaults = array( 
  114. 'in_testing' => false,  
  115. 'allowed_if_flagged' => false,  
  116. 'description' => '',  
  117. 'group' => '',  
  118. 'method' => 'GET',  
  119. 'path' => '/',  
  120. 'min_version' => '0',  
  121. 'max_version' => WPCOM_JSON_API__CURRENT_VERSION,  
  122. 'force' => '',  
  123. 'deprecated' => false,  
  124. 'new_version' => WPCOM_JSON_API__CURRENT_VERSION,  
  125. 'jp_disabled' => false,  
  126. 'path_labels' => array(),  
  127. 'request_format' => array(),  
  128. 'response_format' => array(),  
  129. 'query_parameters' => array(),  
  130. 'version' => 'v1',  
  131. 'example_request' => '',  
  132. 'example_request_data' => '',  
  133. 'example_response' => '',  
  134. 'required_scope' => '',  
  135. 'pass_wpcom_user_details' => false,  
  136. 'can_use_user_details_instead_of_blog_membership' => false,  
  137. 'custom_fields_filtering' => false,  
  138. 'allow_cross_origin_request' => false,  
  139. 'allow_unauthorized_request' => false,  
  140. 'allow_jetpack_site_auth' => false,  
  141. ); 
  142.  
  143. $args = wp_parse_args( $args, $defaults ); 
  144.  
  145. $this->in_testing = $args['in_testing']; 
  146.  
  147. $this->allowed_if_flagged = $args['allowed_if_flagged']; 
  148.  
  149. $this->description = $args['description']; 
  150. $this->group = $args['group']; 
  151. $this->stat = $args['stat']; 
  152. $this->force = $args['force']; 
  153. $this->jp_disabled = $args['jp_disabled']; 
  154.  
  155. $this->method = $args['method']; 
  156. $this->path = $args['path']; 
  157. $this->path_labels = $args['path_labels']; 
  158. $this->min_version = $args['min_version']; 
  159. $this->max_version = $args['max_version']; 
  160. $this->deprecated = $args['deprecated']; 
  161. $this->new_version = $args['new_version']; 
  162.  
  163. $this->pass_wpcom_user_details = $args['pass_wpcom_user_details']; 
  164. $this->custom_fields_filtering = (bool) $args['custom_fields_filtering']; 
  165. $this->can_use_user_details_instead_of_blog_membership = $args['can_use_user_details_instead_of_blog_membership']; 
  166.  
  167. $this->allow_cross_origin_request = (bool) $args['allow_cross_origin_request']; 
  168. $this->allow_unauthorized_request = (bool) $args['allow_unauthorized_request']; 
  169. $this->allow_jetpack_site_auth = (bool) $args['allow_jetpack_site_auth']; 
  170.  
  171. $this->version = $args['version']; 
  172.  
  173. $this->required_scope = $args['required_scope']; 
  174.  
  175. if ( $this->request_format ) { 
  176. $this->request_format = array_filter( array_merge( $this->request_format, $args['request_format'] ) ); 
  177. } else { 
  178. $this->request_format = $args['request_format']; 
  179.  
  180. if ( $this->response_format ) { 
  181. $this->response_format = array_filter( array_merge( $this->response_format, $args['response_format'] ) ); 
  182. } else { 
  183. $this->response_format = $args['response_format']; 
  184.  
  185. if ( false === $args['query_parameters'] ) { 
  186. $this->query = array(); 
  187. } elseif ( is_array( $args['query_parameters'] ) ) { 
  188. $this->query = array_filter( array_merge( $this->query, $args['query_parameters'] ) ); 
  189.  
  190. $this->api = WPCOM_JSON_API::init(); // Auto-add to WPCOM_JSON_API 
  191.  
  192. /** Example Request/Response ******************************************/ 
  193.  
  194. // Examples for endpoint documentation request 
  195. $this->example_request = $args['example_request']; 
  196. $this->example_request_data = $args['example_request_data']; 
  197. $this->example_response = $args['example_response']; 
  198.  
  199. $this->api->add( $this ); 
  200.  
  201. // Get all query args. Prefill with defaults 
  202. function query_args( $return_default_values = true, $cast_and_filter = true ) { 
  203. $args = array_intersect_key( $this->api->query, $this->query ); 
  204.  
  205. if ( !$cast_and_filter ) { 
  206. return $args; 
  207.  
  208. return $this->cast_and_filter( $args, $this->query, $return_default_values ); 
  209.  
  210. // Get POST body data 
  211. function input( $return_default_values = true, $cast_and_filter = true ) { 
  212. $input = trim( $this->api->post_body ); 
  213. $content_type = $this->api->content_type; 
  214. if ( $content_type ) { 
  215. list ( $content_type ) = explode( ';', $content_type ); 
  216. $content_type = trim( $content_type ); 
  217. switch ( $content_type ) { 
  218. case 'application/json' : 
  219. case 'application/x-javascript' : 
  220. case 'text/javascript' : 
  221. case 'text/x-javascript' : 
  222. case 'text/x-json' : 
  223. case 'text/json' : 
  224. $return = json_decode( $input, true ); 
  225.  
  226. if ( function_exists( 'json_last_error' ) ) { 
  227. if ( JSON_ERROR_NONE !== json_last_error() ) { 
  228. return null; 
  229. } else { 
  230. if ( is_null( $return ) && json_encode( null ) !== $input ) { 
  231. return null; 
  232.  
  233. break; 
  234. case 'multipart/form-data' : 
  235. $return = array_merge( stripslashes_deep( $_POST ), $_FILES ); 
  236. break; 
  237. case 'application/x-www-form-urlencoded' : 
  238. //attempt JSON first, since probably a curl command 
  239. $return = json_decode( $input, true ); 
  240.  
  241. if ( is_null( $return ) ) { 
  242. wp_parse_str( $input, $return ); 
  243.  
  244. break; 
  245. default : 
  246. wp_parse_str( $input, $return ); 
  247. break; 
  248.  
  249. if ( !$cast_and_filter ) { 
  250. return $return; 
  251.  
  252. return $this->cast_and_filter( $return, $this->request_format, $return_default_values ); 
  253.  
  254. function cast_and_filter( $data, $documentation, $return_default_values = false, $for_output = false ) { 
  255. $return_as_object = false; 
  256. if ( is_object( $data ) ) { 
  257. // @todo this should probably be a deep copy if $data can ever have nested objects 
  258. $data = (array) $data; 
  259. $return_as_object = true; 
  260. } elseif ( !is_array( $data ) ) { 
  261. return $data; 
  262.  
  263. $boolean_arg = array( 'false', 'true' ); 
  264. $naeloob_arg = array( 'true', 'false' ); 
  265.  
  266. $return = array(); 
  267.  
  268. foreach ( $documentation as $key => $description ) { 
  269. if ( is_array( $description ) ) { 
  270. // String or boolean array keys only 
  271. $whitelist = array_keys( $description ); 
  272.  
  273. if ( $whitelist === $boolean_arg || $whitelist === $naeloob_arg ) { 
  274. // Truthiness 
  275. if ( isset( $data[$key] ) ) { 
  276. $return[$key] = (bool) WPCOM_JSON_API::is_truthy( $data[$key] ); 
  277. } elseif ( $return_default_values ) { 
  278. $return[$key] = $whitelist === $naeloob_arg; // Default to true for naeloob_arg and false for boolean_arg. 
  279. } elseif ( isset( $data[$key] ) && isset( $description[$data[$key]] ) ) { 
  280. // String Key 
  281. $return[$key] = (string) $data[$key]; 
  282. } elseif ( $return_default_values ) { 
  283. // Default value 
  284. $return[$key] = (string) current( $whitelist ); 
  285.  
  286. continue; 
  287.  
  288. $types = $this->parse_types( $description ); 
  289. $type = array_shift( $types ); 
  290.  
  291. // Explicit default - string and int only for now. Always set these reguardless of $return_default_values 
  292. if ( isset( $type['default'] ) ) { 
  293. if ( !isset( $data[$key] ) ) { 
  294. $data[$key] = $type['default']; 
  295.  
  296. if ( !isset( $data[$key] ) ) { 
  297. continue; 
  298.  
  299. $this->cast_and_filter_item( $return, $type, $key, $data[$key], $types, $for_output ); 
  300.  
  301. if ( $return_as_object ) { 
  302. return (object) $return; 
  303.  
  304. return $return; 
  305.  
  306. /** 
  307. * Casts $value according to $type. 
  308. * Handles fallbacks for certain values of $type when $value is not that $type 
  309. * Currently, only handles fallback between string <-> array (two way), from string -> false (one way), and from object -> false (one way) 
  310. * 
  311. * Handles "child types" - array:URL, object:category 
  312. * array:URL means an array of URLs 
  313. * object:category means a hash of categories 
  314. * 
  315. * Handles object typing - object>post means an object of type post 
  316. */ 
  317. function cast_and_filter_item( &$return, $type, $key, $value, $types = array(), $for_output = false ) { 
  318. if ( is_string( $type ) ) { 
  319. $type = compact( 'type' ); 
  320.  
  321. switch ( $type['type'] ) { 
  322. case 'false' : 
  323. $return[$key] = false; 
  324. break; 
  325. case 'url' : 
  326. $return[$key] = (string) esc_url_raw( $value ); 
  327. break; 
  328. case 'string' : 
  329. // Fallback string -> array 
  330. if ( is_array( $value ) ) { 
  331. if ( !empty( $types[0] ) ) { 
  332. $next_type = array_shift( $types ); 
  333. return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output ); 
  334.  
  335. // Fallback string -> false 
  336. if ( !is_string( $value ) ) { 
  337. if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) { 
  338. $next_type = array_shift( $types ); 
  339. return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output ); 
  340. $return[$key] = (string) $value; 
  341. break; 
  342. case 'html' : 
  343. $return[$key] = (string) $value; 
  344. break; 
  345. case 'safehtml' : 
  346. $return[$key] = wp_kses( (string) $value, wp_kses_allowed_html() ); 
  347. break; 
  348. case 'media' : 
  349. if ( is_array( $value ) ) { 
  350. if ( isset( $value['name'] ) ) { 
  351. // It's a $_FILES array 
  352. // Reformat into array of $_FILES items 
  353.  
  354. $files = array(); 
  355. foreach ( $value['name'] as $k => $v ) { 
  356. $files[$k] = array(); 
  357. foreach ( array_keys( $value ) as $file_key ) { 
  358. $files[$k][$file_key] = $value[$file_key][$k]; 
  359.  
  360. $return[$key] = $files; 
  361. break; 
  362. } else { 
  363. // no break - treat as 'array' 
  364. // nobreak 
  365. case 'array' : 
  366. // Fallback array -> string 
  367. if ( is_string( $value ) ) { 
  368. if ( !empty( $types[0] ) ) { 
  369. $next_type = array_shift( $types ); 
  370. return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output ); 
  371.  
  372. if ( isset( $type['children'] ) ) { 
  373. $children = array(); 
  374. foreach ( (array) $value as $k => $child ) { 
  375. $this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output ); 
  376. $return[$key] = (array) $children; 
  377. break; 
  378.  
  379. $return[$key] = (array) $value; 
  380. break; 
  381. case 'iso 8601 datetime' : 
  382. case 'datetime' : 
  383. // (string)s 
  384. $dates = $this->parse_date( (string) $value ); 
  385. if ( $for_output ) { 
  386. $return[$key] = $this->format_date( $dates[1], $dates[0] ); 
  387. } else { 
  388. list( $return[$key], $return["{$key}_gmt"] ) = $dates; 
  389. break; 
  390. case 'float' : 
  391. $return[$key] = (float) $value; 
  392. break; 
  393. case 'int' : 
  394. case 'integer' : 
  395. $return[$key] = (int) $value; 
  396. break; 
  397. case 'bool' : 
  398. case 'boolean' : 
  399. $return[$key] = (bool) WPCOM_JSON_API::is_truthy( $value ); 
  400. break; 
  401. case 'object' : 
  402. // Fallback object -> false 
  403. if ( is_scalar( $value ) || is_null( $value ) ) { 
  404. if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) { 
  405. return $this->cast_and_filter_item( $return, 'false', $key, $value, $types, $for_output ); 
  406.  
  407. if ( isset( $type['children'] ) ) { 
  408. $children = array(); 
  409. foreach ( (array) $value as $k => $child ) { 
  410. $this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output ); 
  411. $return[$key] = (object) $children; 
  412. break; 
  413.  
  414. if ( isset( $type['subtype'] ) ) { 
  415. return $this->cast_and_filter_item( $return, $type['subtype'], $key, $value, $types, $for_output ); 
  416.  
  417. $return[$key] = (object) $value; 
  418. break; 
  419. case 'post' : 
  420. $return[$key] = (object) $this->cast_and_filter( $value, $this->post_object_format, false, $for_output ); 
  421. break; 
  422. case 'comment' : 
  423. $return[$key] = (object) $this->cast_and_filter( $value, $this->comment_object_format, false, $for_output ); 
  424. break; 
  425. case 'tag' : 
  426. case 'category' : 
  427. $docs = array( 
  428. 'ID' => '(int)',  
  429. 'name' => '(string)',  
  430. 'slug' => '(string)',  
  431. 'description' => '(HTML)',  
  432. 'post_count' => '(int)',  
  433. 'meta' => '(object)',  
  434. ); 
  435. if ( 'category' === $type['type'] ) { 
  436. $docs['parent'] = '(int)'; 
  437. $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output ); 
  438. break; 
  439. case 'post_reference' : 
  440. case 'comment_reference' : 
  441. $docs = array( 
  442. 'ID' => '(int)',  
  443. 'type' => '(string)',  
  444. 'title' => '(string)',  
  445. 'link' => '(URL)',  
  446. ); 
  447. $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output ); 
  448. break; 
  449. case 'geo' : 
  450. $docs = array( 
  451. 'latitude' => '(float)',  
  452. 'longitude' => '(float)',  
  453. 'address' => '(string)',  
  454. ); 
  455. $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output ); 
  456. break; 
  457. case 'author' : 
  458. $docs = array( 
  459. 'ID' => '(int)',  
  460. 'user_login' => '(string)',  
  461. 'login' => '(string)',  
  462. 'email' => '(string|false)',  
  463. 'name' => '(string)',  
  464. 'first_name' => '(string)',  
  465. 'last_name' => '(string)',  
  466. 'nice_name' => '(string)',  
  467. 'URL' => '(URL)',  
  468. 'avatar_URL' => '(URL)',  
  469. 'profile_URL' => '(URL)',  
  470. 'is_super_admin' => '(bool)',  
  471. 'roles' => '(array:string)' 
  472. ); 
  473. $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output ); 
  474. break; 
  475. case 'role' : 
  476. $docs = array( 
  477. 'name' => '(string)',  
  478. 'display_name' => '(string)',  
  479. 'capabilities' => '(object:boolean)',  
  480. ); 
  481. $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output ); 
  482. break; 
  483. case 'attachment' : 
  484. $docs = array( 
  485. 'ID' => '(int)',  
  486. 'URL' => '(URL)',  
  487. 'guid' => '(string)',  
  488. 'mime_type' => '(string)',  
  489. 'width' => '(int)',  
  490. 'height' => '(int)',  
  491. 'duration' => '(int)',  
  492. ); 
  493. $return[$key] = (object) $this->cast_and_filter( 
  494. $value,  
  495. /** 
  496. * Filter the documentation returned for a post attachment. 
  497. * 
  498. * @module json-api 
  499. * 
  500. * @since 1.9.0 
  501. * 
  502. * @param array $docs Array of documentation about a post attachment. 
  503. */ 
  504. apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),  
  505. false,  
  506. $for_output 
  507. ); 
  508. break; 
  509. case 'metadata' : 
  510. $docs = array( 
  511. 'id' => '(int)',  
  512. 'key' => '(string)',  
  513. 'value' => '(string|false|float|int|array|object)',  
  514. 'previous_value' => '(string)',  
  515. 'operation' => '(string)',  
  516. ); 
  517. $return[$key] = (object) $this->cast_and_filter( 
  518. $value,  
  519. /** This filter is documented in class.json-api-endpoints.php */ 
  520. apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),  
  521. false,  
  522. $for_output 
  523. ); 
  524. break; 
  525. case 'plugin' : 
  526. $docs = array( 
  527. 'id' => '(safehtml) The plugin\'s ID',  
  528. 'slug' => '(safehtml) The plugin\'s Slug',  
  529. 'active' => '(boolean) The plugin status.',  
  530. 'update' => '(object) The plugin update info.',  
  531. 'name' => '(safehtml) The name of the plugin.',  
  532. 'plugin_url' => '(url) Link to the plugin\'s web site.',  
  533. 'version' => '(safehtml) The plugin version number.',  
  534. 'description' => '(safehtml) Description of what the plugin does and/or notes from the author',  
  535. 'author' => '(safehtml) The plugin author\'s name',  
  536. 'author_url' => '(url) The plugin author web site address',  
  537. 'network' => '(boolean) Whether the plugin can only be activated network wide.',  
  538. 'autoupdate' => '(boolean) Whether the plugin is auto updated',  
  539. 'log' => '(array:safehtml) An array of update log strings.',  
  540. ); 
  541. $return[$key] = (object) $this->cast_and_filter( 
  542. $value,  
  543. /** 
  544. * Filter the documentation returned for a plugin. 
  545. * 
  546. * @module json-api 
  547. * 
  548. * @since 3.1.0 
  549. * 
  550. * @param array $docs Array of documentation about a plugin. 
  551. */ 
  552. apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),  
  553. false,  
  554. $for_output 
  555. ); 
  556. break; 
  557. case 'jetpackmodule' : 
  558. $docs = array( 
  559. 'id' => '(string) The module\'s ID',  
  560. 'active' => '(boolean) The module\'s status.',  
  561. 'name' => '(string) The module\'s name.',  
  562. 'description' => '(safehtml) The module\'s description.',  
  563. 'sort' => '(int) The module\'s display order.',  
  564. 'introduced' => '(string) The Jetpack version when the module was introduced.',  
  565. 'changed' => '(string) The Jetpack version when the module was changed.',  
  566. 'free' => '(boolean) The module\'s Free or Paid status.',  
  567. 'module_tags' => '(array) The module\'s tags.' 
  568. ); 
  569. $return[$key] = (object) $this->cast_and_filter( 
  570. $value,  
  571. /** This filter is documented in class.json-api-endpoints.php */ 
  572. apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),  
  573. false,  
  574. $for_output 
  575. ); 
  576. break; 
  577. case 'sharing_button' : 
  578. $docs = array( 
  579. 'ID' => '(string)',  
  580. 'name' => '(string)',  
  581. 'URL' => '(string)',  
  582. 'icon' => '(string)',  
  583. 'enabled' => '(bool)',  
  584. 'visibility' => '(string)',  
  585. ); 
  586. $return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output ); 
  587. break; 
  588. case 'sharing_button_service': 
  589. $docs = array( 
  590. 'ID' => '(string) The service identifier',  
  591. 'name' => '(string) The service name',  
  592. 'class_name' => '(string) Class name for custom style sharing button elements',  
  593. 'genericon' => '(string) The Genericon unicode character for the custom style sharing button icon',  
  594. 'preview_smart' => '(string) An HTML snippet of a rendered sharing button smart preview',  
  595. 'preview_smart_js' => '(string) An HTML snippet of the page-wide initialization scripts used for rendering the sharing button smart preview' 
  596. ); 
  597. $return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output ); 
  598. break; 
  599.  
  600. default : 
  601. $method_name = $type['type'] . '_docs'; 
  602. if ( method_exists( WPCOM_JSON_API_Jetpack_Overrides, $method_name ) ) { 
  603. $docs = WPCOM_JSON_API_Jetpack_Overrides::$method_name(); 
  604.  
  605. if ( ! empty( $docs ) ) { 
  606. $return[$key] = (object) $this->cast_and_filter( 
  607. $value,  
  608. /** This filter is documented in class.json-api-endpoints.php */ 
  609. apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),  
  610. false,  
  611. $for_output 
  612. ); 
  613. } else { 
  614. trigger_error( "Unknown API casting type {$type['type']}", E_USER_WARNING ); 
  615.  
  616. function parse_types( $text ) { 
  617. if ( !preg_match( '#^\(([^)]+)\)#', ltrim( $text ), $matches ) ) { 
  618. return 'none'; 
  619.  
  620. $types = explode( '|', strtolower( $matches[1] ) ); 
  621. $return = array(); 
  622. foreach ( $types as $type ) { 
  623. foreach ( array( ':' => 'children', '>' => 'subtype', '=' => 'default' ) as $operator => $meaning ) { 
  624. if ( false !== strpos( $type, $operator ) ) { 
  625. $item = explode( $operator, $type, 2 ); 
  626. $return[] = array( 'type' => $item[0], $meaning => $item[1] ); 
  627. continue 2; 
  628. $return[] = compact( 'type' ); 
  629.  
  630. return $return; 
  631.  
  632. /** 
  633. * Checks if the endpoint is publicly displayable 
  634. */ 
  635. function is_publicly_documentable() { 
  636. return '__do_not_document' !== $this->group && true !== $this->in_testing; 
  637.  
  638. /** 
  639. * Auto generates documentation based on description, method, path, path_labels, and query parameters. 
  640. * Echoes HTML. 
  641. */ 
  642. function document( $show_description = true ) { 
  643. global $wpdb; 
  644. $original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset'; 
  645. unset( $GLOBALS['post'] ); 
  646.  
  647. $doc = $this->generate_documentation(); 
  648.  
  649. if ( $show_description ) : 
  650. ?> 
  651. <caption> 
  652. <h1><?php echo wp_kses_post( $doc['method'] ); ?> <?php echo wp_kses_post( $doc['path_labeled'] ); ?></h1> 
  653. <p><?php echo wp_kses_post( $doc['description'] ); ?></p> 
  654. </caption> 
  655.  
  656. <?php endif; ?> 
  657.  
  658. <?php if ( true === $this->deprecated ) { ?> 
  659. <p><strong>This endpoint is deprecated in favor of version <?php echo floatval( $this->new_version ); ?></strong></p> 
  660. <?php } ?> 
  661.  
  662. <section class="resource-info"> 
  663. <h2 id="apidoc-resource-info">Resource Information</h2> 
  664.  
  665. <table class="api-doc api-doc-resource-parameters api-doc-resource"> 
  666.  
  667. <thead> 
  668. <tr> 
  669. <th class="api-index-title" scope="column"> </th> 
  670. <th class="api-index-title" scope="column"> </th> 
  671. </tr> 
  672. </thead> 
  673. <tbody> 
  674.  
  675. <tr class="api-index-item"> 
  676. <th scope="row" class="parameter api-index-item-title">Method</th> 
  677. <td class="type api-index-item-title"><?php echo wp_kses_post( $doc['method'] ); ?></td> 
  678. </tr> 
  679.  
  680. <tr class="api-index-item"> 
  681. <th scope="row" class="parameter api-index-item-title">URL</th> 
  682. <?php 
  683. $version = WPCOM_JSON_API__CURRENT_VERSION; 
  684. if ( !empty( $this->max_version ) ) { 
  685. $version = $this->max_version; 
  686. ?> 
  687. <td class="type api-index-item-title">https://public-api.wordpress.com/rest/v<?php echo floatval( $version ); ?><?php echo wp_kses_post( $doc['path_labeled'] ); ?></td> 
  688. </tr> 
  689.  
  690. <tr class="api-index-item"> 
  691. <th scope="row" class="parameter api-index-item-title">Requires authentication?</th> 
  692. <?php 
  693. $requires_auth = $wpdb->get_row( $wpdb->prepare( "SELECT requires_authentication FROM rest_api_documentation WHERE `version` = %s AND `path` = %s AND `method` = %s LIMIT 1", $version, untrailingslashit( $doc['path_labeled'] ), $doc['method'] ) ); 
  694. ?> 
  695. <td class="type api-index-item-title"><?php echo ( true === (bool) $requires_auth->requires_authentication ? 'Yes' : 'No' ); ?></td> 
  696. </tr> 
  697.  
  698. </tbody> 
  699. </table> 
  700.  
  701. </section> 
  702.  
  703. <?php 
  704.  
  705. foreach ( array( 
  706. 'path' => 'Method Parameters',  
  707. 'query' => 'Query Parameters',  
  708. 'body' => 'Request Parameters',  
  709. 'response' => 'Response Parameters',  
  710. ) as $doc_section_key => $label ) : 
  711. $doc_section = 'response' === $doc_section_key ? $doc['response']['body'] : $doc['request'][$doc_section_key]; 
  712. if ( !$doc_section ) { 
  713. continue; 
  714.  
  715. $param_label = strtolower( str_replace( ' ', '-', $label ) ); 
  716. ?> 
  717.  
  718. <section class="<?php echo $param_label; ?>"> 
  719.  
  720. <h2 id="apidoc-<?php echo esc_attr( $doc_section_key ); ?>"><?php echo wp_kses_post( $label ); ?></h2> 
  721.  
  722. <table class="api-doc api-doc-<?php echo $param_label; ?>-parameters api-doc-<?php echo strtolower( str_replace( ' ', '-', $doc['group'] ) ); ?>"> 
  723.  
  724. <thead> 
  725. <tr> 
  726. <th class="api-index-title" scope="column">Parameter</th> 
  727. <th class="api-index-title" scope="column">Type</th> 
  728. <th class="api-index-title" scope="column">Description</th> 
  729. </tr> 
  730. </thead> 
  731. <tbody> 
  732.  
  733. <?php foreach ( $doc_section as $key => $item ) : ?> 
  734.  
  735. <tr class="api-index-item"> 
  736. <th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $key ); ?></th> 
  737. <td class="type api-index-item-title"><?php echo wp_kses_post( $item['type'] ); // @todo auto-link? ?></td> 
  738. <td class="description api-index-item-body"><?php 
  739.  
  740. $this->generate_doc_description( $item['description'] ); 
  741.  
  742. ?></td> 
  743. </tr> 
  744.  
  745. <?php endforeach; ?> 
  746. </tbody> 
  747. </table> 
  748. </section> 
  749. <?php endforeach; ?> 
  750.  
  751. <?php 
  752. if ( 'unset' !== $original_post ) { 
  753. $GLOBALS['post'] = $original_post; 
  754.  
  755. function add_http_build_query_to_php_content_example( $matches ) { 
  756. $trimmed_match = ltrim( $matches[0] ); 
  757. $pad = substr( $matches[0], 0, -1 * strlen( $trimmed_match ) ); 
  758. $pad = ltrim( $pad, ' ' ); 
  759. $return = ' ' . str_replace( "\n", "\n ", $matches[0] ); 
  760. return " http_build_query({$return}{$pad})"; 
  761.  
  762. /** 
  763. * Recursively generates the <dl>'s to document item descriptions. 
  764. * Echoes HTML. 
  765. */ 
  766. function generate_doc_description( $item ) { 
  767. if ( is_array( $item ) ) : ?> 
  768.  
  769. <dl> 
  770. <?php foreach ( $item as $description_key => $description_value ) : ?> 
  771.  
  772. <dt><?php echo wp_kses_post( $description_key . ':' ); ?></dt> 
  773. <dd><?php $this->generate_doc_description( $description_value ); ?></dd> 
  774.  
  775. <?php endforeach; ?> 
  776.  
  777. </dl> 
  778.  
  779. <?php 
  780. else : 
  781. echo wp_kses_post( $item ); 
  782. endif; 
  783.  
  784. /** 
  785. * Auto generates documentation based on description, method, path, path_labels, and query parameters. 
  786. * Echoes HTML. 
  787. */ 
  788. function generate_documentation() { 
  789. $format = str_replace( '%d', '%s', $this->path ); 
  790. $path_labeled = $format; 
  791. if ( ! empty( $this->path_labels ) ) { 
  792. $path_labeled = vsprintf( $format, array_keys( $this->path_labels ) ); 
  793. $boolean_arg = array( 'false', 'true' ); 
  794. $naeloob_arg = array( 'true', 'false' ); 
  795.  
  796. $doc = array( 
  797. 'description' => $this->description,  
  798. 'method' => $this->method,  
  799. 'path_format' => $this->path,  
  800. 'path_labeled' => $path_labeled,  
  801. 'group' => $this->group,  
  802. 'request' => array( 
  803. 'path' => array(),  
  804. 'query' => array(),  
  805. 'body' => array(),  
  806. ),  
  807. 'response' => array( 
  808. 'body' => array(),  
  809. ); 
  810.  
  811. foreach ( array( 'path_labels' => 'path', 'query' => 'query', 'request_format' => 'body', 'response_format' => 'body' ) as $_property => $doc_item ) { 
  812. foreach ( (array) $this->$_property as $key => $description ) { 
  813. if ( is_array( $description ) ) { 
  814. $description_keys = array_keys( $description ); 
  815. if ( $boolean_arg === $description_keys || $naeloob_arg === $description_keys ) { 
  816. $type = '(bool)'; 
  817. } else { 
  818. $type = '(string)'; 
  819.  
  820. if ( 'response_format' !== $_property ) { 
  821. // hack - don't show "(default)" in response format 
  822. reset( $description ); 
  823. $description_key = key( $description ); 
  824. $description[$description_key] = "(default) {$description[$description_key]}"; 
  825. } else { 
  826. $types = $this->parse_types( $description ); 
  827. $type = array(); 
  828. $default = ''; 
  829.  
  830. if ( 'none' == $types ) { 
  831. $types = array(); 
  832. $types[]['type'] = 'none'; 
  833.  
  834. foreach ( $types as $type_array ) { 
  835. $type[] = $type_array['type']; 
  836. if ( isset( $type_array['default'] ) ) { 
  837. $default = $type_array['default']; 
  838. if ( 'string' === $type_array['type'] ) { 
  839. $default = "'$default'"; 
  840. $type = '(' . join( '|', $type ) . ')'; 
  841. $noop = ''; // skip an index in list below 
  842. list( $noop, $description ) = explode( ')', $description, 2 ); 
  843. $description = trim( $description ); 
  844. if ( $default ) { 
  845. $description .= " Default: $default."; 
  846.  
  847. $item = compact( 'type', 'description' ); 
  848.  
  849. if ( 'response_format' === $_property ) { 
  850. $doc['response'][$doc_item][$key] = $item; 
  851. } else { 
  852. $doc['request'][$doc_item][$key] = $item; 
  853.  
  854. return $doc; 
  855.  
  856. function user_can_view_post( $post_id ) { 
  857. $post = get_post( $post_id ); 
  858. if ( !$post || is_wp_error( $post ) ) { 
  859. return false; 
  860.  
  861. if ( 'inherit' === $post->post_status ) { 
  862. $parent_post = get_post( $post->post_parent ); 
  863. $post_status_obj = get_post_status_object( $parent_post->post_status ); 
  864. } else { 
  865. $post_status_obj = get_post_status_object( $post->post_status ); 
  866.  
  867. if ( !$post_status_obj->public ) { 
  868. if ( is_user_logged_in() ) { 
  869. if ( $post_status_obj->protected ) { 
  870. if ( !current_user_can( 'edit_post', $post->ID ) ) { 
  871. return new WP_Error( 'unauthorized', 'User cannot view post', 403 ); 
  872. } elseif ( $post_status_obj->private ) { 
  873. if ( !current_user_can( 'read_post', $post->ID ) ) { 
  874. return new WP_Error( 'unauthorized', 'User cannot view post', 403 ); 
  875. } elseif ( 'trash' === $post->post_status ) { 
  876. if ( !current_user_can( 'edit_post', $post->ID ) ) { 
  877. return new WP_Error( 'unauthorized', 'User cannot view post', 403 ); 
  878. } elseif ( 'auto-draft' === $post->post_status ) { 
  879. //allow auto-drafts 
  880. } else { 
  881. return new WP_Error( 'unauthorized', 'User cannot view post', 403 ); 
  882. } else { 
  883. return new WP_Error( 'unauthorized', 'User cannot view post', 403 ); 
  884.  
  885. if ( 
  886. -1 == get_option( 'blog_public' ) && 
  887. /** 
  888. * Filter access to a specific post. 
  889. * 
  890. * @module json-api 
  891. * 
  892. * @since 3.4.0 
  893. * 
  894. * @param bool current_user_can( 'read_post', $post->ID ) Can the current user access the post. 
  895. * @param WP_Post $post Post data. 
  896. */ 
  897. ! apply_filters( 
  898. 'wpcom_json_api_user_can_view_post',  
  899. current_user_can( 'read_post', $post->ID ),  
  900. $post 
  901. ) { 
  902. return new WP_Error( 'unauthorized', 'User cannot view post', array( 'status_code' => 403, 'error' => 'private_blog' ) ); 
  903.  
  904. if ( strlen( $post->post_password ) && !current_user_can( 'edit_post', $post->ID ) ) { 
  905. return new WP_Error( 'unauthorized', 'User cannot view password protected post', array( 'status_code' => 403, 'error' => 'password_protected' ) ); 
  906.  
  907. return true; 
  908.  
  909. /** 
  910. * Returns author object. 
  911. * 
  912. * @param $author user ID, user row, WP_User object, comment row, post row 
  913. * @param $show_email output the author's email address? 
  914. * 
  915. * @return (object) 
  916. */ 
  917. function get_author( $author, $show_email = false ) { 
  918. if ( isset( $author->comment_author_email ) && !$author->user_id ) { 
  919. $ID = 0; 
  920. $login = ''; 
  921. $email = $author->comment_author_email; 
  922. $name = $author->comment_author; 
  923. $first_name = ''; 
  924. $last_name = ''; 
  925. $URL = $author->comment_author_url; 
  926. $profile_URL = 'http://en.gravatar.com/' . md5( strtolower( trim( $email ) ) ); 
  927. $nice = ''; 
  928. $site_id = -1; 
  929.  
  930. // Comment author URLs and Emails are sent through wp_kses() on save, which replaces "&" with "&" 
  931. // "&" is the only email/URL character altered by wp_kses() 
  932. foreach ( array( 'email', 'URL' ) as $field ) { 
  933. $$field = str_replace( '&', '&', $$field ); 
  934. } else { 
  935. if ( isset( $author->post_author ) ) { 
  936. // then $author is a Post Object. 
  937. if ( 0 == $author->post_author ) 
  938. return null; 
  939. /** 
  940. * Filter whether the current site is a Jetpack site. 
  941. * 
  942. * @module json-api 
  943. * 
  944. * @since 3.3.0 
  945. * 
  946. * @param bool false Is the current site a Jetpack site. Default to false. 
  947. * @param int get_current_blog_id() Blog ID. 
  948. */ 
  949. $is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() ); 
  950. $post_id = $author->ID; 
  951. if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) { 
  952. $ID = get_post_meta( $post_id, '_jetpack_post_author_external_id', true ); 
  953. $email = get_post_meta( $post_id, '_jetpack_author_email', true ); 
  954. $login = ''; 
  955. $name = get_post_meta( $post_id, '_jetpack_author', true ); 
  956. $first_name = ''; 
  957. $last_name = ''; 
  958. $URL = ''; 
  959. $nice = ''; 
  960. } else { 
  961. $author = $author->post_author; 
  962. } elseif ( isset( $author->user_id ) && $author->user_id ) { 
  963. $author = $author->user_id; 
  964. } elseif ( isset( $author->user_email ) ) { 
  965. $author = $author->ID; 
  966.  
  967. if ( ! isset( $ID ) ) { 
  968. $user = get_user_by( 'id', $author ); 
  969. if ( ! $user || is_wp_error( $user ) ) { 
  970. trigger_error( 'Unknown user', E_USER_WARNING ); 
  971.  
  972. return null; 
  973. $ID = $user->ID; 
  974. $email = $user->user_email; 
  975. $login = $user->user_login; 
  976. $name = $user->display_name; 
  977. $first_name = $user->first_name; 
  978. $last_name = $user->last_name; 
  979. $URL = $user->user_url; 
  980. $nice = $user->user_nicename; 
  981. if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! $is_jetpack ) { 
  982. $active_blog = get_active_blog_for_user( $ID ); 
  983. $site_id = $active_blog->blog_id; 
  984. $profile_URL = "http://en.gravatar.com/{$login}"; 
  985. } else { 
  986. $profile_URL = 'http://en.gravatar.com/' . md5( strtolower( trim( $email ) ) ); 
  987. $site_id = -1; 
  988.  
  989. $avatar_URL = $this->api->get_avatar_url( $email ); 
  990.  
  991. $email = $show_email ? (string) $email : false; 
  992.  
  993. $author = array( 
  994. 'ID' => (int) $ID,  
  995. 'login' => (string) $login,  
  996. 'email' => $email, // (string|bool) 
  997. 'name' => (string) $name,  
  998. 'first_name' => (string) $first_name,  
  999. 'last_name' => (string) $last_name,  
  1000. 'nice_name' => (string) $nice,  
  1001. 'URL' => (string) esc_url_raw( $URL ),  
  1002. 'avatar_URL' => (string) esc_url_raw( $avatar_URL ),  
  1003. 'profile_URL' => (string) esc_url_raw( $profile_URL ),  
  1004. ); 
  1005.  
  1006. if ($site_id > -1) { 
  1007. $author['site_ID'] = (int) $site_id; 
  1008.  
  1009. return (object) $author; 
  1010.  
  1011. function get_media_item( $media_id ) { 
  1012. $media_item = get_post( $media_id ); 
  1013.  
  1014. if ( !$media_item || is_wp_error( $media_item ) ) 
  1015. return new WP_Error( 'unknown_media', 'Unknown Media', 404 ); 
  1016.  
  1017. $response = array( 
  1018. 'id' => strval( $media_item->ID ),  
  1019. 'date' => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),  
  1020. 'parent' => $media_item->post_parent,  
  1021. 'link' => wp_get_attachment_url( $media_item->ID ),  
  1022. 'title' => $media_item->post_title,  
  1023. 'caption' => $media_item->post_excerpt,  
  1024. 'description' => $media_item->post_content,  
  1025. 'metadata' => wp_get_attachment_metadata( $media_item->ID ),  
  1026. ); 
  1027.  
  1028. if ( defined( 'IS_WPCOM' ) && IS_WPCOM && is_array( $response['metadata'] ) && ! empty( $response['metadata']['file'] ) ) { 
  1029. remove_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10 ); 
  1030. $response['metadata']['file'] = _wp_relative_upload_path( $response['metadata']['file'] ); 
  1031. add_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10, 2 ); 
  1032.  
  1033. $response['meta'] = (object) array( 
  1034. 'links' => (object) array( 
  1035. 'self' => (string) $this->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),  
  1036. 'help' => (string) $this->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),  
  1037. 'site' => (string) $this->get_site_link( $this->api->get_blog_id_for_output() ),  
  1038. ),  
  1039. ); 
  1040.  
  1041. return (object) $response; 
  1042.  
  1043. function get_media_item_v1_1( $media_id ) { 
  1044. $media_item = get_post( $media_id ); 
  1045.  
  1046. if ( ! $media_item || is_wp_error( $media_item ) ) 
  1047. return new WP_Error( 'unknown_media', 'Unknown Media', 404 ); 
  1048.  
  1049. $file = basename( wp_get_attachment_url( $media_item->ID ) ); 
  1050. $file_info = pathinfo( $file ); 
  1051. $ext = $file_info['extension']; 
  1052.  
  1053. $response = array( 
  1054. 'ID' => $media_item->ID,  
  1055. 'URL' => wp_get_attachment_url( $media_item->ID ),  
  1056. 'guid' => $media_item->guid,  
  1057. 'date' => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),  
  1058. 'post_ID' => $media_item->post_parent,  
  1059. 'file' => $file,  
  1060. 'mime_type' => $media_item->post_mime_type,  
  1061. 'extension' => $ext,  
  1062. 'title' => $media_item->post_title,  
  1063. 'caption' => $media_item->post_excerpt,  
  1064. 'description' => $media_item->post_content,  
  1065. 'alt' => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),  
  1066. 'thumbnails' => array() 
  1067. ); 
  1068.  
  1069. if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif' ) ) ) { 
  1070. $metadata = wp_get_attachment_metadata( $media_item->ID ); 
  1071. if ( isset( $metadata['height'], $metadata['width'] ) ) { 
  1072. $response['height'] = $metadata['height']; 
  1073. $response['width'] = $metadata['width']; 
  1074. if ( is_array( $metadata['sizes'] ) ) { 
  1075. foreach ( $metadata['sizes'] as $size => $size_details ) { 
  1076. $response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file']; 
  1077. $response['exif'] = $metadata['image_meta']; 
  1078.  
  1079. if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ) ) ) { 
  1080. $metadata = wp_get_attachment_metadata( $media_item->ID ); 
  1081. $response['length'] = $metadata['length']; 
  1082. $response['exif'] = $metadata; 
  1083.  
  1084. if ( in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) ) ) { 
  1085. $metadata = wp_get_attachment_metadata( $media_item->ID ); 
  1086. if ( isset( $metadata['height'], $metadata['width'] ) ) { 
  1087. $response['height'] = $metadata['height']; 
  1088. $response['width'] = $metadata['width']; 
  1089.  
  1090. if ( isset( $metadata['length'] ) ) { 
  1091. $response['length'] = $metadata['length']; 
  1092.  
  1093. // add VideoPress info 
  1094. if ( function_exists( 'video_get_info_by_blogpostid' ) ) { 
  1095. $info = video_get_info_by_blogpostid( $this->api->get_blog_id_for_output(), $media_id ); 
  1096.  
  1097. // Thumbnails 
  1098. if ( function_exists( 'video_format_done' ) && function_exists( 'video_image_url_by_guid' ) ) { 
  1099. $response['thumbnails'] = array( 'fmt_hd' => '', 'fmt_dvd' => '', 'fmt_std' => '' ); 
  1100. foreach ( $response['thumbnails'] as $size => $thumbnail_url ) { 
  1101. if ( video_format_done( $info, $size ) ) { 
  1102. $response['thumbnails'][ $size ] = video_image_url_by_guid( $info->guid, $size ); 
  1103. } else { 
  1104. unset( $response['thumbnails'][ $size ] ); 
  1105.  
  1106. $response['videopress_guid'] = $info->guid; 
  1107. $response['videopress_processing_done'] = true; 
  1108. if ( '0000-00-00 00:00:00' == $info->finish_date_gmt ) { 
  1109. $response['videopress_processing_done'] = false; 
  1110.  
  1111. $response['thumbnails'] = (object) $response['thumbnails']; 
  1112.  
  1113. $response['meta'] = (object) array( 
  1114. 'links' => (object) array( 
  1115. 'self' => (string) $this->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),  
  1116. 'help' => (string) $this->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),  
  1117. 'site' => (string) $this->get_site_link( $this->api->get_blog_id_for_output() ),  
  1118. ),  
  1119. ); 
  1120.  
  1121. // add VideoPress link to the meta 
  1122. if ( in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) ) ) { 
  1123. if ( function_exists( 'video_get_info_by_blogpostid' ) ) { 
  1124. $response['meta']->links->videopress = (string) $this->get_link( '/videos/%s', $response['videopress_guid'], '' ); 
  1125.  
  1126. if ( $media_item->post_parent > 0 ) { 
  1127. $response['meta']->links->parent = (string) $this->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent ); 
  1128.  
  1129. return (object) $response; 
  1130.  
  1131. function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) { 
  1132.  
  1133. $taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type ); 
  1134. /// keep updating this function 
  1135. if ( !$taxonomy || is_wp_error( $taxonomy ) ) { 
  1136. return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 ); 
  1137.  
  1138. return $this->format_taxonomy( $taxonomy, $taxonomy_type, $context ); 
  1139.  
  1140. function format_taxonomy( $taxonomy, $taxonomy_type, $context ) { 
  1141. // Permissions 
  1142. switch ( $context ) { 
  1143. case 'edit' : 
  1144. $tax = get_taxonomy( $taxonomy_type ); 
  1145. if ( !current_user_can( $tax->cap->edit_terms ) ) 
  1146. return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 ); 
  1147. break; 
  1148. case 'display' : 
  1149. if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) { 
  1150. return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 ); 
  1151. break; 
  1152. default : 
  1153. return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 ); 
  1154.  
  1155. $response = array(); 
  1156. $response['ID'] = (int) $taxonomy->term_id; 
  1157. $response['name'] = (string) $taxonomy->name; 
  1158. $response['slug'] = (string) $taxonomy->slug; 
  1159. $response['description'] = (string) $taxonomy->description; 
  1160. $response['post_count'] = (int) $taxonomy->count; 
  1161.  
  1162. if ( 'category' === $taxonomy_type ) 
  1163. $response['parent'] = (int) $taxonomy->parent; 
  1164.  
  1165. $response['meta'] = (object) array( 
  1166. 'links' => (object) array( 
  1167. 'self' => (string) $this->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type ),  
  1168. 'help' => (string) $this->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type, 'help' ),  
  1169. 'site' => (string) $this->get_site_link( $this->api->get_blog_id_for_output() ),  
  1170. ),  
  1171. ); 
  1172.  
  1173. return (object) $response; 
  1174.  
  1175. /** 
  1176. * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00 
  1177. * 
  1178. * @param $date_gmt (string) GMT datetime string. 
  1179. * @param $date (string) Optional. Used to calculate the offset from GMT. 
  1180. * 
  1181. * @return string 
  1182. */ 
  1183. function format_date( $date_gmt, $date = null ) { 
  1184. $timestamp_gmt = strtotime( "$date_gmt+0000" ); 
  1185.  
  1186. if ( null === $date ) { 
  1187. $timestamp = $timestamp_gmt; 
  1188. $hours = $minutes = $west = 0; 
  1189. } else { 
  1190. $date_time = date_create( "$date+0000" ); 
  1191. if ( $date_time ) { 
  1192. $timestamp = date_format( $date_time, 'U' ); 
  1193. } else { 
  1194. $timestamp = 0; 
  1195.  
  1196. // "0000-00-00 00:00:00" == -62169984000 
  1197. if ( -62169984000 == $timestamp_gmt ) { 
  1198. // WordPress sets post_date=now, post_date_gmt="0000-00-00 00:00:00" for all drafts 
  1199. // WordPress sets post_modified=now, post_modified_gmt="0000-00-00 00:00:00" for new drafts 
  1200.  
  1201. // Try to guess the correct offset from the blog's options. 
  1202. $timezone_string = get_option( 'timezone_string' ); 
  1203.  
  1204. if ( $timezone_string && $date_time ) { 
  1205. $timezone = timezone_open( $timezone_string ); 
  1206. if ( $timezone ) { 
  1207. $offset = $timezone->getOffset( $date_time ); 
  1208. } else { 
  1209. $offset = 3600 * get_option( 'gmt_offset' ); 
  1210. } else { 
  1211. $offset = $timestamp - $timestamp_gmt; 
  1212.  
  1213. $west = $offset < 0; 
  1214. $offset = abs( $offset ); 
  1215. $hours = (int) floor( $offset / 3600 ); 
  1216. $offset -= $hours * 3600; 
  1217. $minutes = (int) floor( $offset / 60 ); 
  1218.  
  1219. return (string) gmdate( 'Y-m-d\\TH:i:s', $timestamp ) . sprintf( '%s%02d:%02d', $west ? '-' : '+', $hours, $minutes ); 
  1220.  
  1221. /** 
  1222. * Parses a date string and returns the local and GMT representations 
  1223. * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without 
  1224. * timezones or offsets. If the parsed datetime was not localized to a 
  1225. * particular timezone or offset we will assume it was given in GMT 
  1226. * relative to now and will convert it to local time using either the 
  1227. * timezone set in the options table for the blog or the GMT offset. 
  1228. * 
  1229. * @param datetime string 
  1230. * 
  1231. * @return array( $local_time_string, $gmt_time_string ) 
  1232. */ 
  1233. function parse_date( $date_string ) { 
  1234. $date_string_info = date_parse( $date_string ); 
  1235. if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) { 
  1236. // Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP. 
  1237. if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) { 
  1238. $dt_local = clone $dt_utc = new DateTime( $date_string ); 
  1239. $dt_utc->setTimezone( new DateTimeZone( 'UTC' ) ); 
  1240. return array( 
  1241. (string) $dt_local->format( 'Y-m-d H:i:s' ),  
  1242. (string) $dt_utc->format( 'Y-m-d H:i:s' ),  
  1243. ); 
  1244.  
  1245. // It's parseable but no TZ info so assume UTC 
  1246. $dt_local = clone $dt_utc = new DateTime( $date_string, new DateTimeZone( 'UTC' ) ); 
  1247. } else { 
  1248. // Could not parse time, use now in UTC 
  1249. $dt_local = clone $dt_utc = new DateTime( 'now', new DateTimeZone( 'UTC' ) ); 
  1250.  
  1251. // First try to use timezone as it's daylight savings aware. 
  1252. $timezone_string = get_option( 'timezone_string' ); 
  1253. if ( $timezone_string ) { 
  1254. $tz = timezone_open( $timezone_string ); 
  1255. if ( $tz ) { 
  1256. $dt_local->setTimezone( $tz ); 
  1257. return array( 
  1258. (string) $dt_local->format( 'Y-m-d H:i:s' ),  
  1259. (string) $dt_utc->format( 'Y-m-d H:i:s' ),  
  1260. ); 
  1261.  
  1262. // Fallback to GMT offset (in hours) 
  1263. // NOTE: TZ of $dt_local is still UTC, we simply modified the timestamp with an offset. 
  1264. $gmt_offset_seconds = intval( get_option( 'gmt_offset' ) * 3600 ); 
  1265. $dt_local->modify("+{$gmt_offset_seconds} seconds"); 
  1266. return array( 
  1267. (string) $dt_local->format( 'Y-m-d H:i:s' ),  
  1268. (string) $dt_utc->format( 'Y-m-d H:i:s' ),  
  1269. ); 
  1270.  
  1271. // Load the functions.php file for the current theme to get its post formats, CPTs, etc. 
  1272. function load_theme_functions() { 
  1273. // bail if we've done this already (can happen when calling /batch endpoint) 
  1274. if ( defined( 'REST_API_THEME_FUNCTIONS_LOADED' ) ) 
  1275. return; 
  1276.  
  1277. define( 'REST_API_THEME_FUNCTIONS_LOADED', true ); 
  1278.  
  1279. // the theme info we care about is found either within functions.php or one of the jetpack files. 
  1280. $function_files = array( '/functions.php', '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php' ); 
  1281.  
  1282. $copy_dirs = array( get_template_directory() ); 
  1283. if ( wpcom_is_vip() ) { 
  1284. $copy_dirs[] = WP_CONTENT_DIR . '/themes/vip/plugins/'; 
  1285.  
  1286. // Is this a child theme? Load the child theme's functions file. 
  1287. if ( get_stylesheet_directory() !== get_template_directory() && wpcom_is_child_theme() ) { 
  1288. foreach ( $function_files as $function_file ) { 
  1289. if ( file_exists( get_stylesheet_directory() . $function_file ) ) { 
  1290. require_once( get_stylesheet_directory() . $function_file ); 
  1291. $copy_dirs[] = get_stylesheet_directory(); 
  1292.  
  1293. foreach ( $function_files as $function_file ) { 
  1294. if ( file_exists( get_template_directory() . $function_file ) ) { 
  1295. require_once( get_template_directory() . $function_file ); 
  1296.  
  1297. // add inc/wpcom.php and/or includes/wpcom.php 
  1298. wpcom_load_theme_compat_file(); 
  1299.  
  1300. // since the stuff we care about (CPTS, post formats, are usually on setup or init hooks, we want to load those) 
  1301. $this->copy_hooks( 'after_setup_theme', 'restapi_theme_after_setup_theme', $copy_dirs ); 
  1302.  
  1303. /** 
  1304. * Fires functions hooked onto `after_setup_theme` by the theme for the purpose of the REST API. 
  1305. * 
  1306. * The REST API does not load the theme when processing requests. 
  1307. * To enable theme-based functionality, the API will load the '/functions.php',  
  1308. * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files 
  1309. * of the theme (parent and child) and copy functions hooked onto 'after_setup_theme' within those files. 
  1310. * 
  1311. * @module json-api 
  1312. * 
  1313. * @since 3.2.0 
  1314. */ 
  1315. do_action( 'restapi_theme_after_setup_theme' ); 
  1316. $this->copy_hooks( 'init', 'restapi_theme_init', $copy_dirs ); 
  1317.  
  1318. /** 
  1319. * Fires functions hooked onto `init` by the theme for the purpose of the REST API. 
  1320. * 
  1321. * The REST API does not load the theme when processing requests. 
  1322. * To enable theme-based functionality, the API will load the '/functions.php',  
  1323. * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files 
  1324. * of the theme (parent and child) and copy functions hooked onto 'init' within those files. 
  1325. * 
  1326. * @module json-api 
  1327. * 
  1328. * @since 3.2.0 
  1329. */ 
  1330. do_action( 'restapi_theme_init' ); 
  1331.  
  1332. function copy_hooks( $from_hook, $to_hook, $base_paths ) { 
  1333. global $wp_filter; 
  1334. foreach ( $wp_filter as $hook => $actions ) { 
  1335. if ( $from_hook <> $hook ) 
  1336. continue; 
  1337. foreach ( (array) $actions as $priority => $callbacks ) { 
  1338. foreach( $callbacks as $callback_key => $callback_data ) { 
  1339. $callback = $callback_data['function']; 
  1340. $reflection = $this->get_reflection( $callback ); // use reflection api to determine filename where function is defined 
  1341. if ( false !== $reflection ) { 
  1342. $file_name = $reflection->getFileName(); 
  1343. foreach( $base_paths as $base_path ) { 
  1344. if ( 0 === strpos( $file_name, $base_path ) ) { // only copy hooks with functions which are part of the specified files 
  1345. $wp_filter[ $to_hook ][ $priority ][ 'cph' . $callback_key ] = $callback_data; 
  1346.  
  1347. function get_reflection( $callback ) { 
  1348. if ( is_array( $callback ) ) { 
  1349. list( $class, $method ) = $callback; 
  1350. return new ReflectionMethod( $class, $method ); 
  1351.  
  1352. if ( is_string( $callback ) && strpos( $callback, "::" ) !== false ) { 
  1353. list( $class, $method ) = explode( "::", $callback ); 
  1354. return new ReflectionMethod( $class, $method ); 
  1355.  
  1356. if ( version_compare( PHP_VERSION, "5.3.0", ">=" ) && method_exists( $callback, "__invoke" ) ) { 
  1357. return new ReflectionMethod( $callback, "__invoke" ); 
  1358.  
  1359. if ( is_string( $callback ) && strpos( $callback, "::" ) == false && function_exists( $callback ) ) { 
  1360. return new ReflectionFunction( $callback ); 
  1361.  
  1362. return false; 
  1363.  
  1364. /** 
  1365. * Try to find the closest supported version of an endpoint to the current endpoint 
  1366. * 
  1367. * For example, if we were looking at the path /animals/panda: 
  1368. * - if the current endpoint is v1.3 and there is a v1.3 of /animals/%s available, we return 1.3 
  1369. * - if the current endpoint is v1.3 and there is no v1.3 of /animals/%s known, we fall back to the 
  1370. * maximum available version of /animals/%s, e.g. 1.1 
  1371. * 
  1372. * This method is used in get_link() to construct meta links for API responses. 
  1373. * 
  1374. * @param $path string The current endpoint path, relative to the version 
  1375. * @param $method string Request method used to access the endpoint path 
  1376. * @return string The current version, or otherwise the maximum version available 
  1377. */ 
  1378. function get_closest_version_of_endpoint( $path, $request_method = 'GET' ) { 
  1379.  
  1380. $path = untrailingslashit( $path ); 
  1381.  
  1382. // /help is a special case - always use the current request version 
  1383. if ( wp_endswith( $path, '/help' ) ) { 
  1384. return $this->api->version; 
  1385.  
  1386. $endpoint_path_versions = $this->get_endpoint_path_versions(); 
  1387. $last_path_segment = $this->get_last_segment_of_relative_path( $path ); 
  1388. $max_version_found = null; 
  1389.  
  1390. foreach ( $endpoint_path_versions as $endpoint_last_path_segment => $endpoints ) { 
  1391.  
  1392. // Does the last part of the path match the path key? (e.g. 'posts') 
  1393. // If the last part contains a placeholder (e.g. %s), we want to carry on 
  1394. if ( $last_path_segment != $endpoint_last_path_segment && ! strstr( $endpoint_last_path_segment, '%' ) ) { 
  1395. continue; 
  1396.  
  1397. foreach ( $endpoints as $endpoint ) { 
  1398. // Does the request method match? 
  1399. if ( ! in_array( $request_method, $endpoint['request_methods'] ) ) { 
  1400. continue; 
  1401.  
  1402. $endpoint_path = untrailingslashit( $endpoint['path'] ); 
  1403. $endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path ); 
  1404.  
  1405. if ( ! preg_match( "#^$endpoint_path_regex\$#", $path, $matches ) ) { 
  1406. continue; 
  1407.  
  1408. // Make sure the endpoint exists at the same version 
  1409. if ( version_compare( $this->api->version, $endpoint['min_version'], '>=') && 
  1410. version_compare( $this->api->version, $endpoint['max_version'], '<=') ) { 
  1411. return $this->api->version; 
  1412.  
  1413. // If the endpoint doesn't exist at the same version, record the max version we found 
  1414. if ( empty( $max_version_found ) || version_compare( $max_version_found, $endpoint['max_version'], '<' ) ) { 
  1415. $max_version_found = $endpoint['max_version']; 
  1416.  
  1417. // If the endpoint version is less than the requested endpoint version, return the max version found 
  1418. if ( ! empty( $max_version_found ) ) { 
  1419. return $max_version_found; 
  1420.  
  1421. // Otherwise, use the API version of the current request 
  1422. return $this->api->version; 
  1423.  
  1424. /** 
  1425. * Get an array of endpoint paths with their associated versions 
  1426. * 
  1427. * The result is cached for 30 minutes. 
  1428. * 
  1429. * @return array Array of endpoint paths, min_versions and max_versions, keyed by last segment of path 
  1430. **/ 
  1431. protected function get_endpoint_path_versions() { 
  1432.  
  1433. // Do we already have the result of this method in the cache? 
  1434. $cache_result = get_transient( 'endpoint_path_versions' ); 
  1435.  
  1436. if ( ! empty ( $cache_result ) ) { 
  1437. return $cache_result; 
  1438.  
  1439. /** 
  1440. * Create a map of endpoints and their min/max versions keyed by the last segment of the path (e.g. 'posts') 
  1441. * This reduces the search space when finding endpoint matches in get_closest_version_of_endpoint() 
  1442. */ 
  1443. $endpoint_path_versions = array(); 
  1444.  
  1445. foreach ( $this->api->endpoints as $key => $endpoint_objects ) { 
  1446.  
  1447. // The key contains a serialized path, min_version and max_version 
  1448. list( $path, $min_version, $max_version ) = unserialize( $key ); 
  1449.  
  1450. // Grab the last component of the relative path to use as the top-level key 
  1451. $last_path_segment = $this->get_last_segment_of_relative_path( $path ); 
  1452.  
  1453. $endpoint_path_versions[ $last_path_segment ][] = array( 
  1454. 'path' => $path,  
  1455. 'min_version' => $min_version,  
  1456. 'max_version' => $max_version,  
  1457. 'request_methods' => array_keys( $endpoint_objects ) 
  1458. ); 
  1459.  
  1460. set_transient( 
  1461. 'endpoint_path_versions',  
  1462. $endpoint_path_versions,  
  1463. (HOUR_IN_SECONDS / 2) 
  1464. ); 
  1465.  
  1466. return $endpoint_path_versions; 
  1467.  
  1468. /** 
  1469. * Grab the last segment of a relative path 
  1470. * 
  1471. * @param string $path Path 
  1472. * @return string Last path segment 
  1473. */ 
  1474. protected function get_last_segment_of_relative_path( $path) { 
  1475. $path_parts = array_filter( explode( '/', $path ) ); 
  1476.  
  1477. if ( empty( $path_parts ) ) { 
  1478. return null; 
  1479.  
  1480. return end( $path_parts ); 
  1481.  
  1482. /** 
  1483. * Generate a URL to an endpoint 
  1484. * 
  1485. * Used to construct meta links in API responses 
  1486. * 
  1487. * @param mixed $args Optional arguments to be appended to URL 
  1488. * @return string Endpoint URL 
  1489. **/ 
  1490. function get_link() { 
  1491. $args = func_get_args(); 
  1492. $format = array_shift( $args ); 
  1493. $base = WPCOM_JSON_API__BASE; 
  1494.  
  1495. $path = array_pop( $args ); 
  1496.  
  1497. if ( $path ) { 
  1498. $path = '/' . ltrim( $path, '/' ); 
  1499.  
  1500. $args[] = $path; 
  1501.  
  1502. // Escape any % in args before using sprintf 
  1503. $escaped_args = array(); 
  1504. foreach ( $args as $arg_key => $arg_value ) { 
  1505. $escaped_args[ $arg_key ] = str_replace( '%', '%%', $arg_value ); 
  1506.  
  1507. $relative_path = vsprintf( "$format%s", $escaped_args ); 
  1508.  
  1509. if ( ! wp_startswith( $relative_path, '.' ) ) { 
  1510. // Generic version. Match the requested version as best we can 
  1511. $api_version = $this->get_closest_version_of_endpoint( $relative_path ); 
  1512. $base = substr( $base, 0, - 1 ) . $api_version; 
  1513.  
  1514. // escape any % in the relative path before running it through sprintf again 
  1515. $relative_path = str_replace( '%', '%%', $relative_path ); 
  1516. // http, WPCOM_JSON_API__BASE, ... , path 
  1517. // %s , %s , $format, %s 
  1518. return esc_url_raw( sprintf( "%s://%s$relative_path", $this->api->public_api_scheme, $base ) ); 
  1519.  
  1520. function get_me_link( $path = '' ) { 
  1521. return $this->get_link( '/me', $path ); 
  1522.  
  1523. function get_taxonomy_link( $blog_id, $taxonomy_id, $taxonomy_type, $path = '' ) { 
  1524. if ( 'category' === $taxonomy_type ) 
  1525. return $this->get_link( '/sites/%d/categories/slug:%s', $blog_id, $taxonomy_id, $path ); 
  1526. else 
  1527. return $this->get_link( '/sites/%d/tags/slug:%s', $blog_id, $taxonomy_id, $path ); 
  1528.  
  1529. function get_media_link( $blog_id, $media_id, $path = '' ) { 
  1530. return $this->get_link( '/sites/%d/media/%d', $blog_id, $media_id, $path ); 
  1531.  
  1532. function get_site_link( $blog_id, $path = '' ) { 
  1533. return $this->get_link( '/sites/%d', $blog_id, $path ); 
  1534.  
  1535. function get_post_link( $blog_id, $post_id, $path = '' ) { 
  1536. return $this->get_link( '/sites/%d/posts/%d', $blog_id, $post_id, $path ); 
  1537.  
  1538. function get_comment_link( $blog_id, $comment_id, $path = '' ) { 
  1539. return $this->get_link( '/sites/%d/comments/%d', $blog_id, $comment_id, $path ); 
  1540.  
  1541. function get_publicize_connection_link( $blog_id, $publicize_connection_id, $path = '' ) { 
  1542. return $this->get_link( '.1/sites/%d/publicize-connections/%d', $blog_id, $publicize_connection_id, $path ); 
  1543.  
  1544. function get_publicize_connections_link( $keyring_token_id, $path = '' ) { 
  1545. return $this->get_link( '.1/me/publicize-connections/?keyring_connection_ID=%d', $keyring_token_id, $path ); 
  1546.  
  1547. function get_keyring_connection_link( $keyring_token_id, $path = '' ) { 
  1548. return $this->get_link( '.1/me/keyring-connections/%d', $keyring_token_id, $path ); 
  1549.  
  1550. function get_external_service_link( $external_service, $path = '' ) { 
  1551. return $this->get_link( '.1/meta/external-services/%s', $external_service, $path ); 
  1552.  
  1553.  
  1554. /** 
  1555. * Check whether a user can view or edit a post type 
  1556. * @param string $post_type post type to check 
  1557. * @param string $context 'display' or 'edit' 
  1558. * @return bool 
  1559. */ 
  1560. function current_user_can_access_post_type( $post_type, $context='display' ) { 
  1561. $post_type_object = get_post_type_object( $post_type ); 
  1562. if ( ! $post_type_object ) { 
  1563. return false; 
  1564.  
  1565. switch( $context ) { 
  1566. case 'edit': 
  1567. return current_user_can( $post_type_object->cap->edit_posts ); 
  1568. case 'display': 
  1569. return $post_type_object->public || current_user_can( $post_type_object->cap->read_private_posts ); 
  1570. default: 
  1571. return false; 
  1572.  
  1573. function is_post_type_allowed( $post_type ) { 
  1574. // if the post type is empty, that's fine, WordPress will default to post 
  1575. if ( empty( $post_type ) ) 
  1576. return true; 
  1577.  
  1578. // allow special 'any' type 
  1579. if ( 'any' == $post_type ) 
  1580. return true; 
  1581.  
  1582. // check for allowed types 
  1583. if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) ) 
  1584. return true; 
  1585.  
  1586. return false; 
  1587.  
  1588. /** 
  1589. * Gets the whitelisted post types that JP should allow access to. 
  1590. * 
  1591. * @return array Whitelisted post types. 
  1592. */ 
  1593. protected function _get_whitelisted_post_types() { 
  1594. $allowed_types = array( 'post', 'page', 'revision' ); 
  1595.  
  1596. /** 
  1597. * Filter the post types Jetpack has access to, and can synchronize with WordPress.com. 
  1598. * 
  1599. * @module json-api 
  1600. * 
  1601. * @since 2.2.3 
  1602. * 
  1603. * @param array $allowed_types Array of whitelisted post types. Default to `array( 'post', 'page', 'revision' )`. 
  1604. */ 
  1605. $allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types ); 
  1606.  
  1607. return array_unique( $allowed_types ); 
  1608.  
  1609. function handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs = array(), $force_parent_id = false ) { 
  1610.  
  1611. add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) ); 
  1612.  
  1613. $media_ids = $errors = array(); 
  1614. $user_can_upload_files = current_user_can( 'upload_files' ); 
  1615. $media_attrs = array_values( $media_attrs ); // reset the keys 
  1616. $i = 0; 
  1617.  
  1618. if ( ! empty( $media_files ) ) { 
  1619. $this->api->trap_wp_die( 'upload_error' ); 
  1620. foreach ( $media_files as $media_item ) { 
  1621. $_FILES['.api.media.item.'] = $media_item; 
  1622. if ( ! $user_can_upload_files ) { 
  1623. $media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 ); 
  1624. } else { 
  1625. if ( $force_parent_id ) { 
  1626. $parent_id = absint( $force_parent_id ); 
  1627. } elseif ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) { 
  1628. $parent_id = absint( $media_attrs[$i]['parent_id'] ); 
  1629. } else { 
  1630. $parent_id = 0; 
  1631. $media_id = media_handle_upload( '.api.media.item.', $parent_id ); 
  1632. if ( is_wp_error( $media_id ) ) { 
  1633. $errors[$i]['file'] = $media_item['name']; 
  1634. $errors[$i]['error'] = $media_id->get_error_code(); 
  1635. $errors[$i]['message'] = $media_id->get_error_message(); 
  1636. } else { 
  1637. $media_ids[$i] = $media_id; 
  1638.  
  1639. $i++; 
  1640. $this->api->trap_wp_die( null ); 
  1641. unset( $_FILES['.api.media.item.'] ); 
  1642.  
  1643. if ( ! empty( $media_urls ) ) { 
  1644. foreach ( $media_urls as $url ) { 
  1645. if ( ! $user_can_upload_files ) { 
  1646. $media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 ); 
  1647. } else { 
  1648. if ( $force_parent_id ) { 
  1649. $parent_id = absint( $force_parent_id ); 
  1650. } else if ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) { 
  1651. $parent_id = absint( $media_attrs[$i]['parent_id'] ); 
  1652. } else { 
  1653. $parent_id = 0; 
  1654. $media_id = $this->handle_media_sideload( $url, $parent_id ); 
  1655. if ( is_wp_error( $media_id ) ) { 
  1656. $errors[$i] = array( 
  1657. 'file' => $url,  
  1658. 'error' => $media_id->get_error_code(),  
  1659. 'message' => $media_id->get_error_message(),  
  1660. ); 
  1661. } elseif ( ! empty( $media_id ) ) { 
  1662. $media_ids[$i] = $media_id; 
  1663.  
  1664. $i++; 
  1665.  
  1666. if ( ! empty( $media_attrs ) ) { 
  1667. foreach ( $media_ids as $index => $media_id ) { 
  1668. if ( empty( $media_attrs[$index] ) ) 
  1669. continue; 
  1670.  
  1671. $attrs = $media_attrs[$index]; 
  1672. $insert = array(); 
  1673.  
  1674. if ( ! empty( $attrs['title'] ) ) { 
  1675. $insert['post_title'] = $attrs['title']; 
  1676.  
  1677. if ( ! empty( $attrs['caption'] ) ) 
  1678. $insert['post_excerpt'] = $attrs['caption']; 
  1679.  
  1680. if ( ! empty( $attrs['description'] ) ) 
  1681. $insert['post_content'] = $attrs['description']; 
  1682.  
  1683. if ( empty( $insert ) ) 
  1684. continue; 
  1685.  
  1686. $insert['ID'] = $media_id; 
  1687. wp_update_post( (object) $insert ); 
  1688.  
  1689. return array( 'media_ids' => $media_ids, 'errors' => $errors ); 
  1690.  
  1691.  
  1692. function handle_media_sideload( $url, $parent_post_id = 0 ) { 
  1693. if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) ) 
  1694. return false; 
  1695.  
  1696. // if we didn't get a URL, let's bail 
  1697. $parsed = @parse_url( $url ); 
  1698. if ( empty( $parsed ) ) 
  1699. return false; 
  1700.  
  1701. $tmp = download_url( $url ); 
  1702. if ( is_wp_error( $tmp ) ) { 
  1703. return $tmp; 
  1704.  
  1705. if ( ! file_is_displayable_image( $tmp ) ) { 
  1706. @unlink( $tmp ); 
  1707. return false; 
  1708.  
  1709. // emulate a $_FILES entry 
  1710. $file_array = array( 
  1711. 'name' => basename( parse_url( $url, PHP_URL_PATH ) ),  
  1712. 'tmp_name' => $tmp,  
  1713. ); 
  1714.  
  1715. $id = media_handle_sideload( $file_array, $parent_post_id ); 
  1716. @unlink( $tmp ); 
  1717.  
  1718. if ( is_wp_error( $id ) ) { 
  1719. return $id; 
  1720.  
  1721. if ( ! $id || ! is_int( $id ) ) { 
  1722. return false; 
  1723.  
  1724. return $id; 
  1725.  
  1726. function allow_video_uploads( $mimes ) { 
  1727. // if we are on Jetpack, bail - Videos are already allowed 
  1728. if ( ! defined( 'IS_WPCOM' ) || !IS_WPCOM ) { 
  1729. return $mimes; 
  1730.  
  1731. // extra check that this filter is only ever applied during REST API requests 
  1732. if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) { 
  1733. return $mimes; 
  1734.  
  1735. // bail early if they already have the upgrade.. 
  1736. if ( get_option( 'video_upgrade' ) == '1' ) { 
  1737. return $mimes; 
  1738.  
  1739. // lets whitelist to only specific clients right now 
  1740. $clients_allowed_video_uploads = array(); 
  1741. /** 
  1742. * Filter the list of whitelisted video clients. 
  1743. * 
  1744. * @module json-api 
  1745. * 
  1746. * @since 3.2.0 
  1747. * 
  1748. * @param array $clients_allowed_video_uploads Array of whitelisted Video clients. 
  1749. */ 
  1750. $clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads ); 
  1751. if ( !in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) { 
  1752. return $mimes; 
  1753.  
  1754. $mime_list = wp_get_mime_types(); 
  1755.  
  1756. $video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) ); 
  1757. /** 
  1758. * Filter the video filetypes allowed on the site. 
  1759. * 
  1760. * @module json-api 
  1761. * 
  1762. * @since 3.2.0 
  1763. * 
  1764. * @param array $video_exts Array of video filetypes allowed on the site. 
  1765. */ 
  1766. $video_exts = apply_filters( 'video_upload_filetypes', $video_exts ); 
  1767. $video_mimes = array(); 
  1768.  
  1769. if ( !empty( $video_exts ) ) { 
  1770. foreach ( $video_exts as $ext ) { 
  1771. foreach ( $mime_list as $ext_pattern => $mime ) { 
  1772. if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false ) 
  1773. $video_mimes[$ext_pattern] = $mime; 
  1774.  
  1775. $mimes = array_merge( $mimes, $video_mimes ); 
  1776.  
  1777. return $mimes; 
  1778.  
  1779. function is_current_site_multi_user() { 
  1780. $users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' ); 
  1781. if ( false === $users ) { 
  1782. $user_query = new WP_User_Query( array( 
  1783. 'blog_id' => get_current_blog_id(),  
  1784. 'fields' => 'ID',  
  1785. ) ); 
  1786. $users = (int) $user_query->get_total(); 
  1787. wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS ); 
  1788. return $users > 1; 
  1789.  
  1790. function allows_cross_origin_requests() { 
  1791. return 'GET' == $this->method || $this->allow_cross_origin_request; 
  1792.  
  1793. function allows_unauthorized_requests( $origin, $complete_access_origins ) { 
  1794. return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) ); 
  1795.  
  1796. /** 
  1797. * Return endpoint response 
  1798. * 
  1799. * @param ... determined by ->$path 
  1800. * 
  1801. * @return 
  1802. * falsy: HTTP 500, no response body 
  1803. * WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body 
  1804. * $data: HTTP 200, json_encode( $data ) response body 
  1805. */ 
  1806. abstract function callback( $path = '' ); 
  1807.  
  1808.  
  1809.  
  1810. require_once( dirname( __FILE__ ) . '/json-endpoints.php' ); 
.