Нет описания

class.json-api-endpoints.php 70KB

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