PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.1
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.1
6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 All 155 releases
← All changes | modules/cost-tracker/rest-api/version2/class-mainwp-rest-costs-controller.php +132 -86 6.1.3 → 6.2.1 View file →
@@ -96,8 +96,9 @@
96 96 array(
97 97 'methods' => WP_REST_Server::EDITABLE,
98 98 'callback' => array( $this, 'create_item' ),
99 99 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
100 + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
100 101 ),
101 102 )
102 103 );
103 104
@@ -122,8 +123,9 @@
122 123 array(
123 124 'methods' => WP_REST_Server::EDITABLE,
124 125 'callback' => array( $this, 'update_item' ),
125 126 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
127 + 'args' => $this->get_edit_cost_args(),
126 128 ),
127 129 )
128 130 );
129 131
@@ -289,8 +291,12 @@
289 291 * @param WP_REST_Request $request Full details about the request.
290 292 * @return WP_Error|WP_REST_Response
291 293 */
292 294 public function create_item( $request ) {
295 + // handle_rest_update_insert_item() reads a non-empty id as "update that row", and
296 + // the body bag outranks anything the create route registers, so the id is cleared
297 + // out of every bag before the handler can point a create at an existing cost.
298 + $request->set_param( 'id', null );
293 299 try {
294 300 $resp_data = $this->handle_rest_update_insert_item( $request );
295 301 } catch ( MainWP_Extra_Exception $e ) {
296 302 return new \WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
@@ -338,17 +344,23 @@
338 344 $selected_sites = array();
339 345 $selected_groups = array();
340 346 $selected_clients = array();
341 347
348 + // The admin form persists all three lists and every reader unions the three
349 + // columns, so a chain here would silently drop the second selector a caller sent.
342 350 if ( isset( $request['sites'] ) && is_array( $request['sites'] ) ) {
343 351 foreach ( wp_unslash( $request['sites'] ) as $selected ) {
344 352 $selected_sites[] = intval( $selected );
345 353 }
346 - } elseif ( isset( $request['groups'] ) && is_array( $request['groups'] ) ) {
354 + }
355 +
356 + if ( isset( $request['groups'] ) && is_array( $request['groups'] ) ) {
347 357 foreach ( wp_unslash( $request['groups'] ) as $selected ) {
348 358 $selected_groups[] = intval( $selected );
349 359 }
350 - } elseif ( isset( $request['clients'] ) && is_array( $request['clients'] ) ) {
360 + }
361 +
362 + if ( isset( $request['clients'] ) && is_array( $request['clients'] ) ) {
351 363 foreach ( wp_unslash( $request['clients'] ) as $selected ) {
352 364 $selected_clients[] = intval( $selected );
353 365 }
354 366 }
@@ -405,8 +417,27 @@
405 417 }
406 418
407 419
408 420 /**
421 + * Make the id in the path the id every parameter bag reports.
422 + *
423 + * WP_REST_Request ranks body and query params above URL params, so a body id would
424 + * decide which cost a /costs/{id}/edit or /costs/{id}/remove call writes to, and a
425 + * ?id= query param which cost the read routes return. The batch route carries no
426 + * path id, so its items keep addressing themselves by the id in the item body.
427 + * set_param() overwrites the key in every bag that already holds it.
428 + *
429 + * @param WP_REST_Request $request Full details about the request.
430 + * @return void
431 + */
432 + protected function pin_id_to_path( $request ) {
433 + $url_params = $request->get_url_params();
434 + if ( isset( $url_params['id'] ) ) {
435 + $request->set_param( 'id', (int) $url_params['id'] );
436 + }
437 + }
438 +
439 + /**
409 440 * Get site by.
410 441 *
411 442 * @param WP_REST_Request $request Full details about the request.
412 443 * @return WP_Error|Object Item.
@@ -411,8 +442,9 @@
411 442 * @param WP_REST_Request $request Full details about the request.
412 443 * @return WP_Error|Object Item.
413 444 */
414 445 public function get_request_item( $request ) {
446 + $this->pin_id_to_path( $request );
415 447 $id = $request['id'];
416 448 $item = Cost_Tracker_DB::get_instance()->get_cost_tracker_by( 'id', $id );
417 449 if ( empty( $item ) ) {
418 450 return $this->get_rest_data_error( 'id', 'costs' );
@@ -440,32 +472,8 @@
440 472 );
441 473 }
442 474
443 475 /**
444 - * Adds new client.
445 - *
446 - * @param WP_REST_Request $request Full details about the request.
447 - * @return WP_Error|WP_REST_Response
448 - */
449 - public function add_item( $request ) {
450 - // get data.
451 - $fields = $request->get_json_params();
452 - try {
453 - $data = MainWP_Client_Handler::rest_api_add_client( $fields );
454 - $response = new \WP_REST_Response( $data );
455 - $response->set_status( 200 );
456 - } catch ( \Exception $e ) {
457 - return new WP_Error( $e->getCode(), $e->getMessage(), array( 'status' => 400 ) );
458 - }
459 -
460 - // get data.
461 - $value = MainWP_DB_Client::instance()->get_wp_clients( $prepared_args );
462 - return rest_ensure_response( $value );
463 - }
464 -
465 -
466 -
467 - /**
468 476 * Update item.
469 477 *
470 478 * @param WP_REST_Request $request Full details about the request.
471 479 * @return WP_Error|WP_REST_Response
@@ -643,18 +651,8 @@
643 651 );
644 652 }
645 653
646 654 /**
647 - * Only return writable props from schema.
648 - *
649 - * @param array $schema schema.
650 - * @return bool
651 - */
652 - protected function filter_writable_props( $schema ) {
653 - return empty( $schema['readonly'] );
654 - }
655 -
656 - /**
657 655 * Get the query params for collections.
658 656 *
659 657 * @return array
660 658 */
@@ -771,10 +769,36 @@
771 769 }
772 770
773 771
774 772 /**
775 - * Get the Tags schema, conforming to JSON Schema.
773 + * Arguments registered by the edit route.
776 774 *
775 + * The edit route identifies the cost by its URL segment, so the schema's id arg is
776 + * dropped here rather than letting a body id point the update somewhere else. Schema
777 + * defaults are dropped too: core injects arg defaults as request params, and
778 + * handle_rest_update_insert_item() writes every field it reads, so a defaulted arg
779 + * would overwrite a stored value the caller never sent.
780 + *
781 + * @return array
782 + */
783 + protected function get_edit_cost_args() {
784 + $args = $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE );
785 + unset( $args['id'] );
786 + foreach ( $args as &$arg ) {
787 + unset( $arg['default'] );
788 + }
789 + unset( $arg );
790 + return $args;
791 + }
792 +
793 + /**
794 + * Get the costs schema, conforming to JSON Schema.
795 + *
796 + * The properties are named the way the wire names them, which is what
797 + * handle_rest_update_insert_item() reads and what the docs document. Five of them
798 + * used to carry their DB column name instead, so the args derived from this schema
799 + * validated names no write path ever looked at.
800 + *
777 801 * @since 5.2
778 802 * @return array
779 803 */
780 804 public function get_item_schema() {
@@ -782,146 +806,168 @@
782 806 '$schema' => 'http://json-schema.org/draft-04/schema#',
783 807 'title' => 'costs',
784 808 'type' => 'object',
785 809 'properties' => array(
786 - 'id' => array(
810 + 'id' => array(
787 811 'type' => 'integer',
788 812 'description' => __( 'Cost ID.', 'mainwp' ),
789 813 'sanitize_callback' => 'absint',
790 814 'validate_callback' => 'rest_validate_request_arg',
791 815 'context' => array( 'view', 'edit' ),
816 + // The write routes take the cost from the path, and batch items from the
817 + // id arg get_batch_update_args() adds, so no route accepts it as input.
818 + 'readonly' => true,
792 819 ),
793 - 'name' => array(
820 + 'name' => array(
794 821 'type' => 'string',
795 822 'description' => __( 'Cost name.', 'mainwp' ),
796 823 'sanitize_callback' => 'sanitize_text_field',
797 - 'validate_callback' => 'rest_validatze_request_arg',
824 + 'validate_callback' => 'rest_validate_request_arg',
798 825 'context' => array( 'view', 'edit' ),
799 826 ),
800 - 'url' => array(
827 + 'url' => array(
801 828 'type' => 'string',
802 829 'description' => __( 'Cost url.', 'mainwp' ),
803 830 'sanitize_callback' => 'sanitize_url',
804 - 'validate_callback' => 'rest_validatze_request_arg',
831 + 'validate_callback' => 'rest_validate_request_arg',
805 832 'context' => array( 'view', 'edit' ),
806 833 ),
807 - 'type' => array(
834 + 'payment_type' => array(
808 835 'type' => 'string',
809 - 'description' => __( 'Cost type.', 'mainwp' ),
836 + 'description' => __( 'Payment type.', 'mainwp' ),
810 837 'sanitize_callback' => 'sanitize_text_field',
811 - 'validate_callback' => 'rest_validatze_request_arg',
838 + 'validate_callback' => 'rest_validate_request_arg',
812 839 'context' => array( 'view', 'edit' ),
813 840 ),
814 - 'product_type' => array(
841 + 'product_type' => array(
815 842 'type' => 'string',
816 843 'description' => __( 'Product type.', 'mainwp' ),
817 844 'sanitize_callback' => 'sanitize_text_field',
818 - 'validate_callback' => 'rest_validatze_request_arg',
845 + 'validate_callback' => 'rest_validate_request_arg',
819 846 'context' => array( 'view', 'edit' ),
820 847 ),
821 - 'slug' => array(
848 + 'product_slug' => array(
822 849 'type' => 'string',
823 850 'description' => __( 'Product slug.', 'mainwp' ),
824 851 'sanitize_callback' => 'sanitize_text_field',
825 - 'validate_callback' => 'rest_validatze_request_arg',
852 + 'validate_callback' => 'rest_validate_request_arg',
826 853 'context' => array( 'view', 'edit' ),
827 854 ),
828 - 'license_type' => array(
855 + 'license_type' => array(
829 856 'type' => 'string',
830 857 'description' => __( 'License type.', 'mainwp' ),
831 858 'sanitize_callback' => 'sanitize_text_field',
832 - 'validate_callback' => 'rest_validatze_request_arg',
859 + 'validate_callback' => 'rest_validate_request_arg',
833 860 'context' => array( 'view', 'edit' ),
834 861 ),
835 - 'cost_status' => array(
862 + 'cost_tracker_status' => array(
836 863 'type' => 'string',
837 864 'description' => __( 'Cost status.', 'mainwp' ),
838 865 'sanitize_callback' => 'sanitize_text_field',
839 - 'validate_callback' => 'rest_validatze_request_arg',
866 + 'validate_callback' => 'rest_validate_request_arg',
840 867 'context' => array( 'view', 'edit' ),
841 868 ),
842 - 'payment_method' => array(
869 + 'payment_method' => array(
843 870 'type' => 'string',
844 871 'description' => __( 'Payment method.', 'mainwp' ),
845 872 'sanitize_callback' => 'sanitize_text_field',
846 - 'validate_callback' => 'rest_validatze_request_arg',
873 + 'validate_callback' => 'rest_validate_request_arg',
847 874 'context' => array( 'view', 'edit' ),
848 875 ),
849 - 'price' => array(
876 + 'price' => array(
850 877 'type' => 'number',
851 878 'description' => __( 'Price.', 'mainwp' ),
852 879 'sanitize_callback' => 'sanitize_text_field',
853 - 'validate_callback' => 'rest_validatze_request_arg',
880 + 'validate_callback' => 'rest_validate_request_arg',
854 881 'context' => array( 'view', 'edit' ),
855 882 ),
856 - 'renewal_type' => array(
883 + 'renewal_type' => array(
857 884 'type' => 'string',
858 885 'description' => __( 'Renewal type.', 'mainwp' ),
859 886 'sanitize_callback' => 'sanitize_text_field',
860 - 'validate_callback' => 'rest_validatze_request_arg',
887 + 'validate_callback' => 'rest_validate_request_arg',
861 888 'context' => array( 'view', 'edit' ),
862 889 ),
863 - 'last_renewal' => array(
864 - 'type' => 'integer',
865 - 'description' => __( 'Last renewal.', 'mainwp' ),
866 - 'sanitize_callback' => 'absint',
867 - 'validate_callback' => 'rest_validatze_request_arg',
890 + // Read with strtotime(), and documented as any format it accepts, so an
891 + // integer arg would reject every date string the write routes are given.
892 + 'last_renewal' => array(
893 + 'type' => 'string',
894 + 'description' => __( 'Last renewal date, in any format strtotime() accepts.', 'mainwp' ),
895 + 'sanitize_callback' => 'sanitize_text_field',
896 + 'validate_callback' => 'rest_validate_request_arg',
868 897 'context' => array( 'view', 'edit' ),
869 898 ),
870 - 'next_renewal' => array(
899 + 'next_renewal' => array(
871 900 'type' => 'integer',
872 901 'description' => __( 'Next renewal.', 'mainwp' ),
873 902 'sanitize_callback' => 'absint',
874 - 'validate_callback' => 'rest_validatze_request_arg',
903 + 'validate_callback' => 'rest_validate_request_arg',
875 904 'context' => array( 'view', 'edit' ),
905 + // Derived by the handler and rendered as a date string on read, so a
906 + // read-then-write round trip must not be validated against the integer.
907 + 'readonly' => true,
876 908 ),
877 - 'last_alert' => array(
909 + 'last_alert' => array(
878 910 'type' => 'integer',
879 911 'description' => __( 'Last alert.', 'mainwp' ),
880 912 'sanitize_callback' => 'absint',
881 - 'validate_callback' => 'rest_validatze_request_arg',
913 + 'validate_callback' => 'rest_validate_request_arg',
882 914 'context' => array( 'view', 'edit' ),
915 + // Derived by the handler and rendered as a date string on read, so a
916 + // read-then-write round trip must not be validated against the integer.
917 + 'readonly' => true,
883 918 ),
884 - 'cost_icon' => array(
919 + 'icon_hidden' => array(
885 920 'type' => 'string',
886 921 'description' => __( 'Cost icon.', 'mainwp' ),
887 922 'sanitize_callback' => 'sanitize_text_field',
888 - 'validate_callback' => 'rest_validatze_request_arg',
923 + 'validate_callback' => 'rest_validate_request_arg',
889 924 'context' => array( 'view', 'edit' ),
890 925 ),
891 - 'cost_color' => array(
926 + 'product_color' => array(
892 927 'type' => 'string',
893 928 'description' => __( 'Cost color.', 'mainwp' ),
894 929 'sanitize_callback' => 'sanitize_text_field',
895 - 'validate_callback' => 'rest_validatze_request_arg',
930 + 'validate_callback' => 'rest_validate_request_arg',
896 931 'context' => array( 'view', 'edit' ),
897 932 ),
898 - 'sites' => array(
899 - 'type' => 'string',
933 + // handle_rest_update_insert_item() requires one of these three to be an
934 + // array of ids, so typing them as strings made every validated create fail.
935 + 'sites' => array(
936 + 'type' => 'array',
900 937 'description' => __( 'Sites IDs.', 'mainwp' ),
901 - 'sanitize_callback' => 'sanitize_text_field',
902 - 'validate_callback' => 'rest_validatze_request_arg',
938 + 'items' => array(
939 + 'type' => 'integer',
940 + ),
941 + 'sanitize_callback' => 'wp_parse_id_list',
942 + 'validate_callback' => 'rest_validate_request_arg',
903 943 'context' => array( 'view', 'edit' ),
904 944 ),
905 - 'groups' => array(
906 - 'type' => 'string',
945 + 'groups' => array(
946 + 'type' => 'array',
907 947 'description' => __( 'Groups IDs.', 'mainwp' ),
908 - 'sanitize_callback' => 'sanitize_text_field',
909 - 'validate_callback' => 'rest_validatze_request_arg',
948 + 'items' => array(
949 + 'type' => 'integer',
950 + ),
951 + 'sanitize_callback' => 'wp_parse_id_list',
952 + 'validate_callback' => 'rest_validate_request_arg',
910 953 'context' => array( 'view', 'edit' ),
911 954 ),
912 - 'clients' => array(
913 - 'type' => 'string',
955 + 'clients' => array(
956 + 'type' => 'array',
914 957 'description' => __( 'Clients IDs.', 'mainwp' ),
915 - 'sanitize_callback' => 'sanitize_text_field',
916 - 'validate_callback' => 'rest_validatze_request_arg',
958 + 'items' => array(
959 + 'type' => 'integer',
960 + ),
961 + 'sanitize_callback' => 'wp_parse_id_list',
962 + 'validate_callback' => 'rest_validate_request_arg',
917 963 'context' => array( 'view', 'edit' ),
918 964 ),
919 - 'note' => array(
965 + 'note' => array(
920 966 'type' => 'string',
921 967 'description' => __( 'Note.', 'mainwp' ),
922 968 'sanitize_callback' => 'sanitize_text_field',
923 - 'validate_callback' => 'rest_validatze_request_arg',
969 + 'validate_callback' => 'rest_validate_request_arg',
924 970 'context' => array( 'view', 'edit' ),
925 971 ),
926 972 ),
927 973 );