/wp-content/plugins/badgeos/includes/cmb2/includes/rest-api/CMB2_REST.php

https://github.com/livinglab/openlab · PHP · 794 lines · 306 code · 100 blank · 388 comment · 47 complexity · 6883fda4367c50bdb5db74b901cc2af9 MD5 · raw file

  1. <?php
  2. /**
  3. * Handles hooking CMB2 objects/fields into the WordPres REST API
  4. * which can allow fields to be read and/or updated.
  5. *
  6. * @since 2.2.3
  7. *
  8. * @category WordPress_Plugin
  9. * @package CMB2
  10. * @author CMB2 team
  11. * @license GPL-2.0+
  12. * @link https://cmb2.io
  13. *
  14. * @property-read read_fields Array of readable field objects.
  15. * @property-read edit_fields Array of editable field objects.
  16. * @property-read rest_read Whether CMB2 object is readable via the rest api.
  17. * @property-read rest_edit Whether CMB2 object is editable via the rest api.
  18. */
  19. class CMB2_REST extends CMB2_Hookup_Base {
  20. /**
  21. * The current CMB2 REST endpoint version
  22. *
  23. * @var string
  24. * @since 2.2.3
  25. */
  26. const VERSION = '1';
  27. /**
  28. * The CMB2 REST base namespace (v should always be followed by $version)
  29. *
  30. * @var string
  31. * @since 2.2.3
  32. */
  33. const NAME_SPACE = 'cmb2/v1';
  34. /**
  35. * @var CMB2 object
  36. * @since 2.2.3
  37. */
  38. public $cmb;
  39. /**
  40. * @var CMB2_REST[] objects
  41. * @since 2.2.3
  42. */
  43. protected static $boxes = array();
  44. /**
  45. * @var array Array of cmb ids for each type.
  46. * @since 2.2.3
  47. */
  48. protected static $type_boxes = array(
  49. 'post' => array(),
  50. 'user' => array(),
  51. 'comment' => array(),
  52. 'term' => array(),
  53. );
  54. /**
  55. * Array of readable field objects.
  56. *
  57. * @var CMB2_Field[]
  58. * @since 2.2.3
  59. */
  60. protected $read_fields = array();
  61. /**
  62. * Array of editable field objects.
  63. *
  64. * @var CMB2_Field[]
  65. * @since 2.2.3
  66. */
  67. protected $edit_fields = array();
  68. /**
  69. * Whether CMB2 object is readable via the rest api.
  70. *
  71. * @var boolean
  72. */
  73. protected $rest_read = false;
  74. /**
  75. * Whether CMB2 object is editable via the rest api.
  76. *
  77. * @var boolean
  78. */
  79. protected $rest_edit = false;
  80. /**
  81. * A functionalized constructor, used for the hookup action callbacks.
  82. *
  83. * @since 2.2.6
  84. *
  85. * @param CMB2 $cmb The CMB2 object to hookup
  86. *
  87. * @return CMB2_Hookup_Base $hookup The hookup object.
  88. */
  89. public static function maybe_init_and_hookup( CMB2 $cmb ) {
  90. if ( $cmb->prop( 'show_in_rest' ) && function_exists( 'rest_get_server' ) ) {
  91. $hookup = new self( $cmb );
  92. return $hookup->universal_hooks();
  93. }
  94. return false;
  95. }
  96. /**
  97. * Constructor
  98. *
  99. * @since 2.2.3
  100. *
  101. * @param CMB2 $cmb The CMB2 object to be registered for the API.
  102. */
  103. public function __construct( CMB2 $cmb ) {
  104. $this->cmb = $cmb;
  105. self::$boxes[ $cmb->cmb_id ] = $this;
  106. $show_value = $this->cmb->prop( 'show_in_rest' );
  107. $this->rest_read = self::is_readable( $show_value );
  108. $this->rest_edit = self::is_editable( $show_value );
  109. }
  110. /**
  111. * Hooks to register on frontend and backend.
  112. *
  113. * @since 2.2.3
  114. *
  115. * @return void
  116. */
  117. public function universal_hooks() {
  118. // hook up the CMB rest endpoint classes
  119. $this->once( 'rest_api_init', array( __CLASS__, 'init_routes' ), 0 );
  120. if ( function_exists( 'register_rest_field' ) ) {
  121. $this->once( 'rest_api_init', array( __CLASS__, 'register_cmb2_fields' ), 50 );
  122. }
  123. $this->declare_read_edit_fields();
  124. add_filter( 'is_protected_meta', array( $this, 'is_protected_meta' ), 10, 3 );
  125. return $this;
  126. }
  127. /**
  128. * Initiate the CMB2 Boxes and Fields routes
  129. *
  130. * @since 2.2.3
  131. *
  132. * @return void
  133. */
  134. public static function init_routes() {
  135. $wp_rest_server = rest_get_server();
  136. $boxes_controller = new CMB2_REST_Controller_Boxes( $wp_rest_server );
  137. $boxes_controller->register_routes();
  138. $fields_controller = new CMB2_REST_Controller_Fields( $wp_rest_server );
  139. $fields_controller->register_routes();
  140. }
  141. /**
  142. * Loop through REST boxes and call register_rest_field for each object type.
  143. *
  144. * @since 2.2.3
  145. *
  146. * @return void
  147. */
  148. public static function register_cmb2_fields() {
  149. $alltypes = $taxonomies = array();
  150. foreach ( self::$boxes as $cmb_id => $rest_box ) {
  151. $types = array_flip( $rest_box->cmb->box_types( array( 'post' ) ) );
  152. if ( isset( $types['user'] ) ) {
  153. unset( $types['user'] );
  154. self::$type_boxes['user'][ $cmb_id ] = $cmb_id;
  155. }
  156. if ( isset( $types['comment'] ) ) {
  157. unset( $types['comment'] );
  158. self::$type_boxes['comment'][ $cmb_id ] = $cmb_id;
  159. }
  160. if ( isset( $types['term'] ) ) {
  161. unset( $types['term'] );
  162. $taxonomies = array_merge(
  163. $taxonomies,
  164. CMB2_Utils::ensure_array( $rest_box->cmb->prop( 'taxonomies' ) )
  165. );
  166. self::$type_boxes['term'][ $cmb_id ] = $cmb_id;
  167. }
  168. if ( ! empty( $types ) ) {
  169. $alltypes = array_merge( $alltypes, array_flip( $types ) );
  170. self::$type_boxes['post'][ $cmb_id ] = $cmb_id;
  171. }
  172. }
  173. $alltypes = array_unique( $alltypes );
  174. if ( ! empty( $alltypes ) ) {
  175. self::register_rest_field( $alltypes, 'post' );
  176. }
  177. if ( ! empty( self::$type_boxes['user'] ) ) {
  178. self::register_rest_field( 'user', 'user' );
  179. }
  180. if ( ! empty( self::$type_boxes['comment'] ) ) {
  181. self::register_rest_field( 'comment', 'comment' );
  182. }
  183. if ( ! empty( self::$type_boxes['term'] ) ) {
  184. self::register_rest_field( $taxonomies, 'term' );
  185. }
  186. }
  187. /**
  188. * Wrapper for register_rest_field.
  189. *
  190. * @since 2.2.3
  191. *
  192. * @param string|array $object_types Object(s) the field is being registered
  193. * to, "post"|"term"|"comment" etc.
  194. * @param string $object_types Canonical object type for callbacks.
  195. *
  196. * @return void
  197. */
  198. protected static function register_rest_field( $object_types, $object_type ) {
  199. register_rest_field( $object_types, 'cmb2', array(
  200. 'get_callback' => array( __CLASS__, "get_{$object_type}_rest_values" ),
  201. 'update_callback' => array( __CLASS__, "update_{$object_type}_rest_values" ),
  202. 'schema' => null, // @todo add schema
  203. ) );
  204. }
  205. /**
  206. * Setup readable and editable fields.
  207. *
  208. * @since 2.2.3
  209. *
  210. * @return void
  211. */
  212. protected function declare_read_edit_fields() {
  213. foreach ( $this->cmb->prop( 'fields' ) as $field ) {
  214. $show_in_rest = isset( $field['show_in_rest'] ) ? $field['show_in_rest'] : null;
  215. if ( false === $show_in_rest ) {
  216. continue;
  217. }
  218. if ( $this->can_read( $show_in_rest ) ) {
  219. $this->read_fields[] = $field['id'];
  220. }
  221. if ( $this->can_edit( $show_in_rest ) ) {
  222. $this->edit_fields[] = $field['id'];
  223. }
  224. }
  225. }
  226. /**
  227. * Determines if a field is readable based on it's show_in_rest value
  228. * and the box's show_in_rest value.
  229. *
  230. * @since 2.2.3
  231. *
  232. * @param bool $show_in_rest Field's show_in_rest value. Default null.
  233. *
  234. * @return bool Whether field is readable.
  235. */
  236. protected function can_read( $show_in_rest ) {
  237. // if 'null', then use default box value.
  238. if ( null === $show_in_rest ) {
  239. return $this->rest_read;
  240. }
  241. // Else check if the value represents readable.
  242. return self::is_readable( $show_in_rest );
  243. }
  244. /**
  245. * Determines if a field is editable based on it's show_in_rest value
  246. * and the box's show_in_rest value.
  247. *
  248. * @since 2.2.3
  249. *
  250. * @param bool $show_in_rest Field's show_in_rest value. Default null.
  251. *
  252. * @return bool Whether field is editable.
  253. */
  254. protected function can_edit( $show_in_rest ) {
  255. // if 'null', then use default box value.
  256. if ( null === $show_in_rest ) {
  257. return $this->rest_edit;
  258. }
  259. // Else check if the value represents editable.
  260. return self::is_editable( $show_in_rest );
  261. }
  262. /**
  263. * Handler for getting post custom field data.
  264. *
  265. * @since 2.2.3
  266. *
  267. * @param array $object The object data from the response
  268. * @param string $field_name Name of field
  269. * @param WP_REST_Request $request Current request
  270. * @param string $object_type The request object type
  271. *
  272. * @return mixed
  273. */
  274. public static function get_post_rest_values( $object, $field_name, $request, $object_type ) {
  275. if ( 'cmb2' === $field_name ) {
  276. return self::get_rest_values( $object, $request, $object_type, 'post' );
  277. }
  278. }
  279. /**
  280. * Handler for getting user custom field data.
  281. *
  282. * @since 2.2.3
  283. *
  284. * @param array $object The object data from the response
  285. * @param string $field_name Name of field
  286. * @param WP_REST_Request $request Current request
  287. * @param string $object_type The request object type
  288. *
  289. * @return mixed
  290. */
  291. public static function get_user_rest_values( $object, $field_name, $request, $object_type ) {
  292. if ( 'cmb2' === $field_name ) {
  293. return self::get_rest_values( $object, $request, $object_type, 'user' );
  294. }
  295. }
  296. /**
  297. * Handler for getting comment custom field data.
  298. *
  299. * @since 2.2.3
  300. *
  301. * @param array $object The object data from the response
  302. * @param string $field_name Name of field
  303. * @param WP_REST_Request $request Current request
  304. * @param string $object_type The request object type
  305. *
  306. * @return mixed
  307. */
  308. public static function get_comment_rest_values( $object, $field_name, $request, $object_type ) {
  309. if ( 'cmb2' === $field_name ) {
  310. return self::get_rest_values( $object, $request, $object_type, 'comment' );
  311. }
  312. }
  313. /**
  314. * Handler for getting term custom field data.
  315. *
  316. * @since 2.2.3
  317. *
  318. * @param array $object The object data from the response
  319. * @param string $field_name Name of field
  320. * @param WP_REST_Request $request Current request
  321. * @param string $object_type The request object type
  322. *
  323. * @return mixed
  324. */
  325. public static function get_term_rest_values( $object, $field_name, $request, $object_type ) {
  326. if ( 'cmb2' === $field_name ) {
  327. return self::get_rest_values( $object, $request, $object_type, 'term' );
  328. }
  329. }
  330. /**
  331. * Handler for getting custom field data.
  332. *
  333. * @since 2.2.3
  334. *
  335. * @param array $object The object data from the response
  336. * @param WP_REST_Request $request Current request
  337. * @param string $object_type The request object type
  338. * @param string $main_object_type The cmb main object type
  339. *
  340. * @return mixed
  341. */
  342. protected static function get_rest_values( $object, $request, $object_type, $main_object_type = 'post' ) {
  343. if ( ! isset( $object['id'] ) ) {
  344. return;
  345. }
  346. $values = array();
  347. if ( ! empty( self::$type_boxes[ $main_object_type ] ) ) {
  348. foreach ( self::$type_boxes[ $main_object_type ] as $cmb_id ) {
  349. $rest_box = self::$boxes[ $cmb_id ];
  350. foreach ( $rest_box->read_fields as $field_id ) {
  351. $rest_box->cmb->object_id( $object['id'] );
  352. $rest_box->cmb->object_type( $main_object_type );
  353. $field = $rest_box->cmb->get_field( $field_id );
  354. $field->object_id( $object['id'] );
  355. $field->object_type( $main_object_type );
  356. $values[ $cmb_id ][ $field->id( true ) ] = $field->get_data();
  357. }
  358. }
  359. }
  360. return $values;
  361. }
  362. /**
  363. * Handler for updating post custom field data.
  364. *
  365. * @since 2.2.3
  366. *
  367. * @param mixed $values The value of the field
  368. * @param object $object The object from the response
  369. * @param string $field_name Name of field
  370. * @param WP_REST_Request $request Current request
  371. * @param string $object_type The request object type
  372. *
  373. * @return bool|int
  374. */
  375. public static function update_post_rest_values( $values, $object, $field_name, $request, $object_type ) {
  376. if ( 'cmb2' === $field_name ) {
  377. return self::update_rest_values( $values, $object, $request, $object_type, 'post' );
  378. }
  379. }
  380. /**
  381. * Handler for updating user custom field data.
  382. *
  383. * @since 2.2.3
  384. *
  385. * @param mixed $values The value of the field
  386. * @param object $object The object from the response
  387. * @param string $field_name Name of field
  388. * @param WP_REST_Request $request Current request
  389. * @param string $object_type The request object type
  390. *
  391. * @return bool|int
  392. */
  393. public static function update_user_rest_values( $values, $object, $field_name, $request, $object_type ) {
  394. if ( 'cmb2' === $field_name ) {
  395. return self::update_rest_values( $values, $object, $request, $object_type, 'user' );
  396. }
  397. }
  398. /**
  399. * Handler for updating comment custom field data.
  400. *
  401. * @since 2.2.3
  402. *
  403. * @param mixed $values The value of the field
  404. * @param object $object The object from the response
  405. * @param string $field_name Name of field
  406. * @param WP_REST_Request $request Current request
  407. * @param string $object_type The request object type
  408. *
  409. * @return bool|int
  410. */
  411. public static function update_comment_rest_values( $values, $object, $field_name, $request, $object_type ) {
  412. if ( 'cmb2' === $field_name ) {
  413. return self::update_rest_values( $values, $object, $request, $object_type, 'comment' );
  414. }
  415. }
  416. /**
  417. * Handler for updating term custom field data.
  418. *
  419. * @since 2.2.3
  420. *
  421. * @param mixed $values The value of the field
  422. * @param object $object The object from the response
  423. * @param string $field_name Name of field
  424. * @param WP_REST_Request $request Current request
  425. * @param string $object_type The request object type
  426. *
  427. * @return bool|int
  428. */
  429. public static function update_term_rest_values( $values, $object, $field_name, $request, $object_type ) {
  430. if ( 'cmb2' === $field_name ) {
  431. return self::update_rest_values( $values, $object, $request, $object_type, 'term' );
  432. }
  433. }
  434. /**
  435. * Handler for updating custom field data.
  436. *
  437. * @since 2.2.3
  438. *
  439. * @param mixed $values The value of the field
  440. * @param object $object The object from the response
  441. * @param WP_REST_Request $request Current request
  442. * @param string $object_type The request object type
  443. * @param string $main_object_type The cmb main object type
  444. *
  445. * @return bool|int
  446. */
  447. protected static function update_rest_values( $values, $object, $request, $object_type, $main_object_type = 'post' ) {
  448. if ( empty( $values ) || ! is_array( $values ) ) {
  449. return;
  450. }
  451. $object_id = self::get_object_id( $object, $main_object_type );
  452. if ( ! $object_id ) {
  453. return;
  454. }
  455. $updated = array();
  456. if ( ! empty( self::$type_boxes[ $main_object_type ] ) ) {
  457. foreach ( self::$type_boxes[ $main_object_type ] as $cmb_id ) {
  458. $rest_box = self::$boxes[ $cmb_id ];
  459. if ( ! array_key_exists( $cmb_id, $values ) ) {
  460. continue;
  461. }
  462. $rest_box->cmb->object_id( $object_id );
  463. $rest_box->cmb->object_type( $main_object_type );
  464. $updated[ $cmb_id ] = $rest_box->sanitize_box_values( $values );
  465. }
  466. }
  467. return $updated;
  468. }
  469. /**
  470. * Loop through box fields and sanitize the values.
  471. *
  472. * @since 2.2.o
  473. *
  474. * @param array $values Array of values being provided.
  475. * @return array Array of updated/sanitized values.
  476. */
  477. public function sanitize_box_values( array $values ) {
  478. $updated = array();
  479. $this->cmb->pre_process();
  480. foreach ( $this->edit_fields as $field_id ) {
  481. $updated[ $field_id ] = $this->sanitize_field_value( $values, $field_id );
  482. }
  483. $this->cmb->after_save();
  484. return $updated;
  485. }
  486. /**
  487. * Handles returning a sanitized field value.
  488. *
  489. * @since 2.2.3
  490. *
  491. * @param array $values Array of values being provided.
  492. * @param string $field_id The id of the field to update.
  493. *
  494. * @return mixed The results of saving/sanitizing a field value.
  495. */
  496. protected function sanitize_field_value( array $values, $field_id ) {
  497. if ( ! array_key_exists( $field_id, $values[ $this->cmb->cmb_id ] ) ) {
  498. return;
  499. }
  500. $field = $this->cmb->get_field( $field_id );
  501. if ( 'title' == $field->type() ) {
  502. return;
  503. }
  504. $field->object_id( $this->cmb->object_id() );
  505. $field->object_type( $this->cmb->object_type() );
  506. if ( 'group' == $field->type() ) {
  507. return $this->sanitize_group_value( $values, $field );
  508. }
  509. return $field->save_field( $values[ $this->cmb->cmb_id ][ $field_id ] );
  510. }
  511. /**
  512. * Handles returning a sanitized group field value.
  513. *
  514. * @since 2.2.3
  515. *
  516. * @param array $values Array of values being provided.
  517. * @param CMB2_Field $field CMB2_Field object.
  518. *
  519. * @return mixed The results of saving/sanitizing the group field value.
  520. */
  521. protected function sanitize_group_value( array $values, CMB2_Field $field ) {
  522. $fields = $field->fields();
  523. if ( empty( $fields ) ) {
  524. return;
  525. }
  526. $this->cmb->data_to_save[ $field->_id() ] = $values[ $this->cmb->cmb_id ][ $field->_id() ];
  527. return $this->cmb->save_group_field( $field );
  528. }
  529. /**
  530. * Filter whether a meta key is protected.
  531. *
  532. * @since 2.2.3
  533. *
  534. * @param bool $protected Whether the key is protected. Default false.
  535. * @param string $meta_key Meta key.
  536. * @param string $meta_type Meta type.
  537. */
  538. public function is_protected_meta( $protected, $meta_key, $meta_type ) {
  539. if ( $this->field_can_edit( $meta_key ) ) {
  540. return false;
  541. }
  542. return $protected;
  543. }
  544. protected static function get_object_id( $object, $object_type = 'post' ) {
  545. switch ( $object_type ) {
  546. case 'user':
  547. case 'post':
  548. if ( isset( $object->ID ) ) {
  549. return intval( $object->ID );
  550. }
  551. case 'comment':
  552. if ( isset( $object->comment_ID ) ) {
  553. return intval( $object->comment_ID );
  554. }
  555. case 'term':
  556. if ( is_array( $object ) && isset( $object['term_id'] ) ) {
  557. return intval( $object['term_id'] );
  558. } elseif ( isset( $object->term_id ) ) {
  559. return intval( $object->term_id );
  560. }
  561. }
  562. return 0;
  563. }
  564. /**
  565. * Checks if a given field can be read.
  566. *
  567. * @since 2.2.3
  568. *
  569. * @param string|CMB2_Field $field_id Field ID or CMB2_Field object.
  570. * @param boolean $return_object Whether to return the Field object.
  571. *
  572. * @return mixed False if field can't be read or true|CMB2_Field object.
  573. */
  574. public function field_can_read( $field_id, $return_object = false ) {
  575. return $this->field_can( 'read_fields', $field_id, $return_object );
  576. }
  577. /**
  578. * Checks if a given field can be edited.
  579. *
  580. * @since 2.2.3
  581. *
  582. * @param string|CMB2_Field $field_id Field ID or CMB2_Field object.
  583. * @param boolean $return_object Whether to return the Field object.
  584. *
  585. * @return mixed False if field can't be edited or true|CMB2_Field object.
  586. */
  587. public function field_can_edit( $field_id, $return_object = false ) {
  588. return $this->field_can( 'edit_fields', $field_id, $return_object );
  589. }
  590. /**
  591. * Checks if a given field can be read or edited.
  592. *
  593. * @since 2.2.3
  594. *
  595. * @param string $type Whether we are checking for read or edit fields.
  596. * @param string|CMB2_Field $field_id Field ID or CMB2_Field object.
  597. * @param boolean $return_object Whether to return the Field object.
  598. *
  599. * @return mixed False if field can't be read or edited or true|CMB2_Field object.
  600. */
  601. protected function field_can( $type = 'read_fields', $field_id = '', $return_object = false ) {
  602. if ( ! in_array( $field_id instanceof CMB2_Field ? $field_id->id() : $field_id, $this->{$type}, true ) ) {
  603. return false;
  604. }
  605. return $return_object ? $this->cmb->get_field( $field_id ) : true;
  606. }
  607. /**
  608. * Get a CMB2_REST instance object from the registry by a CMB2 id.
  609. *
  610. * @since 2.2.3
  611. *
  612. * @param string $cmb_id CMB2 config id
  613. *
  614. * @return CMB2_REST|false The CMB2_REST object or false.
  615. */
  616. public static function get_rest_box( $cmb_id ) {
  617. return isset( self::$boxes[ $cmb_id ] ) ? self::$boxes[ $cmb_id ] : false;
  618. }
  619. /**
  620. * Remove a CMB2_REST instance object from the registry.
  621. *
  622. * @since 2.2.3
  623. *
  624. * @param string $cmb_id A CMB2 instance id.
  625. */
  626. public static function remove( $cmb_id ) {
  627. if ( array_key_exists( $cmb_id, self::$boxes ) ) {
  628. unset( self::$boxes[ $cmb_id ] );
  629. }
  630. }
  631. /**
  632. * Retrieve all CMB2_REST instances from the registry.
  633. *
  634. * @since 2.2.3
  635. * @return CMB2[] Array of all registered CMB2_REST instances.
  636. */
  637. public static function get_all() {
  638. return self::$boxes;
  639. }
  640. /**
  641. * Checks if given value is readable.
  642. *
  643. * Value is considered readable if it is not empty and if it does not match the editable blacklist.
  644. *
  645. * @since 2.2.3
  646. *
  647. * @param mixed $value Value to check.
  648. *
  649. * @return boolean Whether value is considered readable.
  650. */
  651. public static function is_readable( $value ) {
  652. return ! empty( $value ) && ! in_array( $value, array(
  653. WP_REST_Server::CREATABLE,
  654. WP_REST_Server::EDITABLE,
  655. WP_REST_Server::DELETABLE,
  656. ), true );
  657. }
  658. /**
  659. * Checks if given value is editable.
  660. *
  661. * Value is considered editable if matches the editable whitelist.
  662. *
  663. * @since 2.2.3
  664. *
  665. * @param mixed $value Value to check.
  666. *
  667. * @return boolean Whether value is considered editable.
  668. */
  669. public static function is_editable( $value ) {
  670. return in_array( $value, array(
  671. WP_REST_Server::EDITABLE,
  672. WP_REST_Server::ALLMETHODS,
  673. ), true );
  674. }
  675. /**
  676. * Magic getter for our object.
  677. *
  678. * @param string $field
  679. * @throws Exception Throws an exception if the field is invalid.
  680. *
  681. * @return mixed
  682. */
  683. public function __get( $field ) {
  684. switch ( $field ) {
  685. case 'read_fields':
  686. case 'edit_fields':
  687. case 'rest_read':
  688. case 'rest_edit':
  689. return $this->{$field};
  690. default:
  691. throw new Exception( 'Invalid ' . __CLASS__ . ' property: ' . $field );
  692. }
  693. }
  694. }