← 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 | ); |