├── .gitignore
├── .phpcs.xml.dist
├── .travis.yml
├── Gruntfile.js
├── README.md
├── bin
└── install-wp-tests.sh
├── composer.json
├── lib
├── class-wp-rest-menu-items-controller.php
├── class-wp-rest-menu-locations-controller.php
└── class-wp-rest-menus-controller.php
├── multisite.xml
├── package.json
├── phpunit.xml.dist
├── plugin.php
└── tests
├── bootstrap.php
├── test-rest-nav-menu-items-controller.php
├── test-rest-nav-menu-locations.php
└── test-rest-nav-menus-controller.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | readme.txt
3 | composer.lock
4 | node_modules
5 | vendor
6 | wp-api
7 |
--------------------------------------------------------------------------------
/.phpcs.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 | Generally-applicable sniffs for WordPress plugins.
4 |
5 |
6 | .
7 | /vendor/
8 | /node_modules/
9 |
10 |
11 | tests/*
12 |
13 |
14 |
15 | lib/*
16 | tests/*
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | dist: trusty
3 |
4 | language: php
5 |
6 | notifications:
7 | email:
8 | on_success: never
9 | on_failure: change
10 |
11 | branches:
12 | only:
13 | - master
14 |
15 | cache:
16 | directories:
17 | - $HOME/.composer/cache
18 |
19 | matrix:
20 | include:
21 | - php: 7.4
22 | env: WP_VERSION=latest
23 | - php: 7.3
24 | env: WP_VERSION=latest
25 | - php: 7.2
26 | env: WP_VERSION=latest
27 | - php: 7.1
28 | env: WP_VERSION=latest
29 | - php: 7.0
30 | env: WP_VERSION=latest
31 | - php: 5.6
32 | env: WP_VERSION=latest
33 | - php: 5.6
34 | env: WP_VERSION=trunk
35 | - php: 5.6
36 | env: WP_TRAVISCI=phpcs
37 | dist: precise
38 |
39 | before_script:
40 | - export PATH="$HOME/.composer/vendor/bin:$PATH"
41 | - composer install
42 | - |
43 | if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then
44 | phpenv config-rm xdebug.ini
45 | else
46 | echo "xdebug.ini does not exist"
47 | fi
48 | - |
49 | if [[ ! -z "$WP_VERSION" ]] ; then
50 | bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION
51 | fi
52 |
53 | script:
54 | - |
55 | if [[ ! -z "$WP_VERSION" ]] ; then
56 | vendor/bin/phpunit
57 | WP_MULTISITE=1 vendor/bin/phpunit
58 | fi
59 | - |
60 | if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then
61 | vendor/bin/phpcs
62 | fi
63 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | /* global module, require */
2 | module.exports = function( grunt ) {
3 |
4 | 'use strict';
5 | require( 'phplint' ).gruntPlugin( grunt );
6 | // Project configuration
7 | grunt.initConfig( {
8 |
9 | pkg: grunt.file.readJSON( 'package.json' ),
10 |
11 | phpcs: {
12 | plugin: {
13 | src: './'
14 | },
15 | options: {
16 | bin: 'vendor/bin/phpcs --extensions=php --ignore="wp-api/*,*/vendor/*,*/node_modules/*"',
17 | standard: 'phpcs.ruleset.xml'
18 | }
19 | },
20 |
21 | phplint: {
22 | options: {
23 | limit: 10,
24 | stdout: true,
25 | stderr: true
26 | },
27 | files: ['lib/**/*.php', 'tests/*.php', '*.php']
28 | },
29 |
30 | phpunit: {
31 | 'default': {
32 | cmd: 'phpunit',
33 | args: ['-c', 'phpunit.xml.dist']
34 | },
35 | 'multisite': {
36 | cmd: 'phpunit',
37 | args: ['-c', 'multisite.xml']
38 | },
39 | 'codecoverage': {
40 | cmd: 'phpunit',
41 | args: ['-c', 'codecoverage.xml']
42 | }
43 | }
44 |
45 | } );
46 | grunt.loadNpmTasks( 'grunt-phpcs' );
47 |
48 | // Testing tasks.
49 | grunt.registerMultiTask( 'phpunit', 'Runs PHPUnit tests, including the ajax, external-http, and multisite tests.', function() {
50 | grunt.util.spawn({
51 | cmd: this.data.cmd,
52 | args: this.data.args,
53 | opts: {stdio: 'inherit'}
54 | }, this.async());
55 | });
56 |
57 | grunt.registerTask( 'test', [ 'phpcs', 'phplint', 'phpunit:default', 'phpunit:multisite' ] );
58 | grunt.util.linefeed = '\n';
59 |
60 | // Travis CI tasks.
61 | grunt.registerTask('travis:phpvalidate', 'Runs PHPUnit Travis CI PHP code tasks.', [
62 | 'phpcs',
63 | 'phplint'
64 | ] );
65 | grunt.registerTask('travis:phpunit', 'Runs PHPUnit Travis CI tasks.', [
66 | 'phpunit:default',
67 | 'phpunit:multisite'
68 | ] );
69 | grunt.registerTask('travis:codecoverage', 'Runs PHPUnit Travis CI Code Coverage task.', [
70 | 'phpunit:codecoverage',
71 | 'phpunit:multisite'
72 | ] );
73 | };
74 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WordPress REST API Navigation Menus Endpoints
2 |
3 | ## Do not use this plugin
4 |
5 | This REST API Menu Endpoints were merged into WordPress Core in version 5.9. To report an issue with those endpoints, [create a trac ticket](https://core.trac.wordpress.org/newticket?component=REST%20API).
6 |
7 | ---
8 |
9 | [](https://travis-ci.org/WP-API/menus-endpoints)
10 |
11 | Feature plugin implementing REST endpoints for WordPress Navigation Menus, Menus Items and Menu locations.
12 | The origins of this projects can be found at trac ticket [#40878](https://core.trac.wordpress.org/ticket/40878).
13 |
14 | To access data from these API, request must be authenticated, not reliving possibility senative data that is not public by default in WordPress.
15 |
16 | ### Endpoints
17 |
18 | Endpoints to define for menus:
19 |
20 | ```
21 | GET /menus
22 | POST /menus
23 | GET /menus/:id
24 | POST /menus/:id
25 | DELETE /menus/:id
26 | ```
27 |
28 | Endpoints to define for menu items:
29 |
30 | ```
31 | GET /menu-items
32 | POST /menu-items
33 | GET /menu-items/:id
34 | POST /menu-items/:id
35 | DELETE /menu-items/:id
36 | ```
37 |
38 |
39 | Endpoints to define for menu locations:
40 |
41 | ```
42 | GET /menu-items
43 | GET /menu-items/:location
44 | ```
45 |
46 | ### License
47 |
48 | The Menus Endpoints is licensed under the GPL v2 or later.
49 |
50 | > This program is free software; you can redistribute it and/or modify
51 | it under the terms of the GNU General Public License, version 2, as
52 | published by the Free Software Foundation.
53 |
54 | > This program is distributed in the hope that it will be useful,
55 | but WITHOUT ANY WARRANTY; without even the implied warranty of
56 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
57 | GNU General Public License for more details.
58 |
59 | > You should have received a copy of the GNU General Public License
60 | along with this program; if not, write to the Free Software
61 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
62 |
63 |
64 | ### Contributions
65 |
66 | Anyone is welcome to contribute to Menus Endpoints
67 |
68 | There are various ways you can contribute:
69 |
70 | * Raise an issue on GitHub.
71 | * Send us a Pull Request with your bug fixes and/or new features.
72 | * Provide feedback and suggestions on enhancements.
73 |
74 | It is worth noting that, this project has travis enabled and runs automated tests, including code sniffing and unit tests. Any pull request will be rejects, unless these tests pass. This is to ensure that the code is of the highest quality, follows coding standards and is secure.
75 |
--------------------------------------------------------------------------------
/bin/install-wp-tests.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if [ $# -lt 3 ]; then
4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]"
5 | exit 1
6 | fi
7 |
8 | DB_NAME=$1
9 | DB_USER=$2
10 | DB_PASS=$3
11 | DB_HOST=${4-localhost}
12 | WP_VERSION=${5-latest}
13 | SKIP_DB_CREATE=${6-false}
14 |
15 | TMPDIR=${TMPDIR-/tmp}
16 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//")
17 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib}
18 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/}
19 |
20 | download() {
21 | if [ `which curl` ]; then
22 | curl -s "$1" > "$2";
23 | elif [ `which wget` ]; then
24 | wget -nv -O "$2" "$1"
25 | fi
26 | }
27 |
28 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then
29 | WP_BRANCH=${WP_VERSION%\-*}
30 | WP_TESTS_TAG="branches/$WP_BRANCH"
31 |
32 | elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
33 | WP_TESTS_TAG="branches/$WP_VERSION"
34 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
35 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
36 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
37 | WP_TESTS_TAG="tags/${WP_VERSION%??}"
38 | else
39 | WP_TESTS_TAG="tags/$WP_VERSION"
40 | fi
41 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
42 | WP_TESTS_TAG="trunk"
43 | else
44 | # http serves a single offer, whereas https serves multiple. we only want one
45 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
46 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
47 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
48 | if [[ -z "$LATEST_VERSION" ]]; then
49 | echo "Latest WordPress version could not be found"
50 | exit 1
51 | fi
52 | WP_TESTS_TAG="tags/$LATEST_VERSION"
53 | fi
54 | set -ex
55 |
56 | install_wp() {
57 |
58 | if [ -d $WP_CORE_DIR ]; then
59 | return;
60 | fi
61 |
62 | mkdir -p $WP_CORE_DIR
63 |
64 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
65 | mkdir -p $TMPDIR/wordpress-nightly
66 | download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip
67 | unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/
68 | mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR
69 | else
70 | if [ $WP_VERSION == 'latest' ]; then
71 | local ARCHIVE_NAME='latest'
72 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then
73 | # https serves multiple offers, whereas http serves single.
74 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json
75 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
76 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
77 | LATEST_VERSION=${WP_VERSION%??}
78 | else
79 | # otherwise, scan the releases and get the most up to date minor version of the major release
80 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'`
81 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1)
82 | fi
83 | if [[ -z "$LATEST_VERSION" ]]; then
84 | local ARCHIVE_NAME="wordpress-$WP_VERSION"
85 | else
86 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION"
87 | fi
88 | else
89 | local ARCHIVE_NAME="wordpress-$WP_VERSION"
90 | fi
91 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz
92 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR
93 | fi
94 |
95 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
96 | }
97 |
98 | install_test_suite() {
99 | # portable in-place argument for both GNU sed and Mac OSX sed
100 | if [[ $(uname -s) == 'Darwin' ]]; then
101 | local ioption='-i.bak'
102 | else
103 | local ioption='-i'
104 | fi
105 |
106 | # set up testing suite if it doesn't yet exist
107 | if [ ! -d $WP_TESTS_DIR ]; then
108 | # set up testing suite
109 | mkdir -p $WP_TESTS_DIR
110 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
111 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
112 | fi
113 |
114 | if [ ! -f wp-tests-config.php ]; then
115 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
116 | # remove all forward slashes in the end
117 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::")
118 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
119 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
120 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
121 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
122 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
123 | fi
124 |
125 | }
126 |
127 | install_db() {
128 |
129 | if [ ${SKIP_DB_CREATE} = "true" ]; then
130 | return 0
131 | fi
132 |
133 | # parse DB_HOST for port or socket references
134 | local PARTS=(${DB_HOST//\:/ })
135 | local DB_HOSTNAME=${PARTS[0]};
136 | local DB_SOCK_OR_PORT=${PARTS[1]};
137 | local EXTRA=""
138 |
139 | if ! [ -z $DB_HOSTNAME ] ; then
140 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
141 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
142 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then
143 | EXTRA=" --socket=$DB_SOCK_OR_PORT"
144 | elif ! [ -z $DB_HOSTNAME ] ; then
145 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
146 | fi
147 | fi
148 |
149 | # create database
150 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
151 | }
152 |
153 | install_wp
154 | install_test_suite
155 | install_db
156 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wp-api/menus-endpoints",
3 | "type": "wordpress-plugin",
4 | "description": "Manage your WordPress menus through the WordPress REST API.",
5 | "homepage": "http://wp-api.org/",
6 | "license": "GPL2+",
7 | "authors": [
8 | {
9 | "name": "WP-API Team",
10 | "homepage": "http://wp-api.org/"
11 | }
12 | ],
13 | "support": {
14 | "issues": "https://github.com/WP-API/WP-API/issues",
15 | "forum": "https://wordpress.org/support/plugin/rest-api"
16 | },
17 | "require": {
18 | "composer/installers": "~1.0"
19 | },
20 | "require-dev": {
21 | "squizlabs/php_codesniffer": "^3.3.1",
22 | "wp-coding-standards/wpcs": "^2.2.0",
23 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0",
24 | "phpcompatibility/phpcompatibility-wp": "^2.0",
25 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
26 | },
27 | "extra": {
28 | "installer-name": "json-rest-api"
29 | },
30 | "scripts": {
31 | "post-install-cmd": "\"vendor/bin/phpcs\" --config-set installed_paths vendor/wp-coding-standards/wpcs",
32 | "post-update-cmd" : "\"vendor/bin/phpcs\" --config-set installed_paths vendor/wp-coding-standards/wpcs"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/lib/class-wp-rest-menu-items-controller.php:
--------------------------------------------------------------------------------
1 | get_nav_menu_item( $id );
25 | }
26 |
27 | /**
28 | * Get the nav menu item, if the ID is valid.
29 | *
30 | * @param int $id Supplied ID.
31 | *
32 | * @return object|WP_Error Post object if ID is valid, WP_Error otherwise.
33 | */
34 | protected function get_nav_menu_item( $id ) {
35 | $post = parent::get_post( $id );
36 | if ( is_wp_error( $post ) ) {
37 | return $post;
38 | }
39 | $nav_item = wp_setup_nav_menu_item( $post );
40 |
41 | return $nav_item;
42 | }
43 |
44 | /**
45 | * Checks if a given request has access to read a menu item if they have access to edit them.
46 | *
47 | * @param WP_REST_Request $request Full details about the request.
48 | * @return bool|WP_Error True if the request has read access for the item, WP_Error object otherwise.
49 | */
50 | public function get_item_permissions_check( $request ) {
51 | $post = $this->get_post( $request['id'] );
52 | if ( is_wp_error( $post ) ) {
53 | return $post;
54 | }
55 | if ( $post && ! $this->check_update_permission( $post ) ) {
56 | return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this menu item, unless you have access to permission edit it. ' ), array( 'status' => rest_authorization_required_code() ) );
57 | }
58 |
59 | return parent::get_item_permissions_check( $request );
60 | }
61 |
62 | /**
63 | * Checks if a given request has access to read menu items if they have access to edit them.
64 | *
65 | * @param WP_REST_Request $request Full details about the request.
66 | * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
67 | */
68 | public function get_items_permissions_check( $request ) {
69 | $post_type = get_post_type_object( $this->post_type );
70 | if ( ! current_user_can( $post_type->cap->edit_posts ) ) {
71 | if ( 'edit' === $request['context'] ) {
72 | return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit posts in this post type.' ), array( 'status' => rest_authorization_required_code() ) );
73 | }
74 | return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view these menu items, unless you have access to permission edit them. ' ), array( 'status' => rest_authorization_required_code() ) );
75 | }
76 | return true;
77 | }
78 |
79 | /**
80 | * Creates a single post.
81 | *
82 | * @param WP_REST_Request $request Full details about the request.
83 | *
84 | * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
85 | */
86 | public function create_item( $request ) {
87 | if ( ! empty( $request['id'] ) ) {
88 | return new WP_Error( 'rest_post_exists', __( 'Cannot create existing post.' ), array( 'status' => 400 ) );
89 | }
90 |
91 | $prepared_nav_item = $this->prepare_item_for_database( $request );
92 |
93 | if ( is_wp_error( $prepared_nav_item ) ) {
94 | return $prepared_nav_item;
95 | }
96 | $prepared_nav_item = (array) $prepared_nav_item;
97 |
98 | $nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], $prepared_nav_item );
99 | if ( is_wp_error( $nav_menu_item_id ) ) {
100 | if ( 'db_insert_error' === $nav_menu_item_id->get_error_code() ) {
101 | $nav_menu_item_id->add_data( array( 'status' => 500 ) );
102 | } else {
103 | $nav_menu_item_id->add_data( array( 'status' => 400 ) );
104 | }
105 |
106 | return $nav_menu_item_id;
107 | }
108 |
109 | $nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
110 | if ( is_wp_error( $nav_menu_item ) ) {
111 | $nav_menu_item->add_data( array( 'status' => 404 ) );
112 |
113 | return $nav_menu_item;
114 | }
115 |
116 | /**
117 | * Fires after a single nav menu item is created or updated via the REST API.
118 | *
119 | * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
120 | *
121 | * @param object $nav_menu_item Inserted or updated nav item object.
122 | * @param WP_REST_Request $request Request object.
123 | * @param bool $creating True when creating a post, false when updating.
124 | * SA
125 | */
126 | do_action( "rest_insert_{$this->post_type}", $nav_menu_item, $request, true );
127 |
128 | $schema = $this->get_item_schema();
129 |
130 | if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
131 | $meta_update = $this->meta->update_value( $request['meta'], $nav_menu_item_id );
132 |
133 | if ( is_wp_error( $meta_update ) ) {
134 | return $meta_update;
135 | }
136 | }
137 |
138 | $nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
139 | $fields_update = $this->update_additional_fields_for_object( $nav_menu_item, $request );
140 |
141 | if ( is_wp_error( $fields_update ) ) {
142 | return $fields_update;
143 | }
144 |
145 | $request->set_param( 'context', 'edit' );
146 |
147 | /**
148 | * Fires after a single nav menu item is completely created or updated via the REST API.
149 | *
150 | * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
151 | *
152 | * @param object $nav_menu_item Inserted or updated nav item object.
153 | * @param WP_REST_Request $request Request object.
154 | * @param bool $creating True when creating a post, false when updating.
155 | */
156 | do_action( "rest_after_insert_{$this->post_type}", $nav_menu_item, $request, true );
157 |
158 | $response = $this->prepare_item_for_response( $nav_menu_item, $request );
159 | $response = rest_ensure_response( $response );
160 |
161 | $response->set_status( 201 );
162 | $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $nav_menu_item_id ) ) );
163 |
164 | return $response;
165 | }
166 |
167 | /**
168 | * Updates a single nav menu item.
169 | *
170 | * @param WP_REST_Request $request Full details about the request.
171 | *
172 | * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
173 | */
174 | public function update_item( $request ) {
175 | $valid_check = $this->get_nav_menu_item( $request['id'] );
176 | if ( is_wp_error( $valid_check ) ) {
177 | return $valid_check;
178 | }
179 |
180 | $prepared_nav_item = $this->prepare_item_for_database( $request );
181 |
182 | if ( is_wp_error( $prepared_nav_item ) ) {
183 | return $prepared_nav_item;
184 | }
185 |
186 | $prepared_nav_item = (array) $prepared_nav_item;
187 |
188 | $nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], $prepared_nav_item );
189 |
190 | if ( is_wp_error( $nav_menu_item_id ) ) {
191 | if ( 'db_update_error' === $nav_menu_item_id->get_error_code() ) {
192 | $nav_menu_item_id->add_data( array( 'status' => 500 ) );
193 | } else {
194 | $nav_menu_item_id->add_data( array( 'status' => 400 ) );
195 | }
196 |
197 | return $nav_menu_item_id;
198 | }
199 |
200 | $nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
201 | if ( is_wp_error( $nav_menu_item ) ) {
202 | $nav_menu_item->add_data( array( 'status' => 404 ) );
203 |
204 | return $nav_menu_item;
205 | }
206 |
207 | /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
208 | do_action( "rest_insert_{$this->post_type}", $nav_menu_item, $request, false );
209 |
210 | $schema = $this->get_item_schema();
211 |
212 | if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
213 | $meta_update = $this->meta->update_value( $request['meta'], $nav_menu_item->ID );
214 |
215 | if ( is_wp_error( $meta_update ) ) {
216 | return $meta_update;
217 | }
218 | }
219 |
220 | $nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
221 | $fields_update = $this->update_additional_fields_for_object( $nav_menu_item, $request );
222 |
223 | if ( is_wp_error( $fields_update ) ) {
224 | return $fields_update;
225 | }
226 |
227 | $request->set_param( 'context', 'edit' );
228 |
229 | /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
230 | do_action( "rest_after_insert_{$this->post_type}", $nav_menu_item, $request, false );
231 |
232 | $response = $this->prepare_item_for_response( $nav_menu_item, $request );
233 |
234 | return rest_ensure_response( $response );
235 | }
236 |
237 | /**
238 | * Deletes a single menu item.
239 | *
240 | * @param WP_REST_Request $request Full details about the request.
241 | * @return true|WP_Error True on success, or WP_Error object on failure.
242 | */
243 | public function delete_item( $request ) {
244 | $menu_item = $this->get_nav_menu_item( $request['id'] );
245 | if ( is_wp_error( $menu_item ) ) {
246 | return $menu_item;
247 | }
248 |
249 | $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
250 |
251 | // We don't support trashing for menu items.
252 | if ( ! $force ) {
253 | /* translators: %s: force=true */
254 | return new WP_Error( 'rest_trash_not_supported', sprintf( __( "Menu items do not support trashing. Set '%s' to delete." ), 'force=true' ), array( 'status' => 501 ) );
255 | }
256 |
257 | $previous = $this->prepare_item_for_response( $menu_item, $request );
258 |
259 | $result = wp_delete_post( $request['id'], true );
260 |
261 | if ( ! $result ) {
262 | return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
263 | }
264 |
265 | $response = new WP_REST_Response();
266 | $response->set_data(
267 | array(
268 | 'deleted' => true,
269 | 'previous' => $previous->get_data(),
270 | )
271 | );
272 |
273 | /**
274 | * Fires immediately after a single menu item is deleted or trashed via the REST API.
275 | *
276 | * They dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
277 | *
278 | * @param Object $menu_item The deleted or trashed menu item.
279 | * @param WP_REST_Response $response The response data.
280 | * @param WP_REST_Request $request The request sent to the API.
281 | */
282 | do_action( "rest_delete_{$this->post_type}", $menu_item, $response, $request );
283 |
284 | return $response;
285 | }
286 |
287 | /**
288 | * Prepares a single post for create or update.
289 | *
290 | * @param WP_REST_Request $request Request object.
291 | *
292 | * @return stdClass|WP_Error
293 | */
294 | protected function prepare_item_for_database( $request ) {
295 | $menu_item_db_id = $request['id'];
296 | $menu_item_obj = $this->get_nav_menu_item( $menu_item_db_id );
297 | // Need to persist the menu item data. See https://core.trac.wordpress.org/ticket/28138 .
298 | if ( ! is_wp_error( $menu_item_obj ) ) {
299 | // Correct the menu position if this was the first item. See https://core.trac.wordpress.org/ticket/28140 .
300 | $position = ( 0 === $menu_item_obj->menu_order ) ? 1 : $menu_item_obj->menu_order;
301 |
302 | $prepared_nav_item = array(
303 | 'menu-item-db-id' => $menu_item_db_id,
304 | 'menu-item-object-id' => $menu_item_obj->object_id,
305 | 'menu-item-object' => $menu_item_obj->object,
306 | 'menu-item-parent-id' => $menu_item_obj->menu_item_parent,
307 | 'menu-item-position' => $position,
308 | 'menu-item-title' => $menu_item_obj->title,
309 | 'menu-item-url' => $menu_item_obj->url,
310 | 'menu-item-description' => $menu_item_obj->description,
311 | 'menu-item-attr-title' => $menu_item_obj->attr_title,
312 | 'menu-item-target' => $menu_item_obj->target,
313 | // Stored in the database as a string.
314 | 'menu-item-classes' => implode( ' ', $menu_item_obj->classes ),
315 | 'menu-item-xfn' => $menu_item_obj->xfn,
316 | 'menu-item-status' => $menu_item_obj->post_status,
317 | 'menu-id' => $this->get_menu_id( $menu_item_db_id ),
318 | );
319 | } else {
320 | $prepared_nav_item = array(
321 | 'menu-id' => 0,
322 | 'menu-item-db-id' => 0,
323 | 'menu-item-object-id' => 0,
324 | 'menu-item-object' => '',
325 | 'menu-item-parent-id' => 0,
326 | 'menu-item-position' => 0,
327 | 'menu-item-type' => 'custom',
328 | 'menu-item-title' => '',
329 | 'menu-item-url' => '',
330 | 'menu-item-description' => '',
331 | 'menu-item-attr-title' => '',
332 | 'menu-item-target' => '',
333 | 'menu-item-classes' => '',
334 | 'menu-item-xfn' => '',
335 | 'menu-item-status' => 'publish',
336 | );
337 | }
338 |
339 | $mapping = array(
340 | 'menu-item-db-id' => 'id',
341 | 'menu-item-object-id' => 'object_id',
342 | 'menu-item-object' => 'object',
343 | 'menu-item-parent-id' => 'parent',
344 | 'menu-item-position' => 'menu_order',
345 | 'menu-item-type' => 'type',
346 | 'menu-item-url' => 'url',
347 | 'menu-item-description' => 'description',
348 | 'menu-item-attr-title' => 'attr_title',
349 | 'menu-item-target' => 'target',
350 | 'menu-item-classes' => 'classes',
351 | 'menu-item-xfn' => 'xfn',
352 | 'menu-item-status' => 'status',
353 | );
354 |
355 | $schema = $this->get_item_schema();
356 |
357 | foreach ( $mapping as $original => $api_request ) {
358 | if ( ! empty( $schema['properties'][ $api_request ] ) && isset( $request[ $api_request ] ) ) {
359 | $check = rest_validate_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] );
360 | if ( is_wp_error( $check ) ) {
361 | $check->add_data( array( 'status' => 400 ) );
362 | return $check;
363 | }
364 | $prepared_nav_item[ $original ] = rest_sanitize_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] );
365 | }
366 | }
367 |
368 | $taxonomy = get_taxonomy( 'nav_menu' );
369 | $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
370 | // If menus submitted, cast to int.
371 | if ( isset( $request[ $base ] ) && ! empty( $request[ $base ] ) ) {
372 | $prepared_nav_item['menu-id'] = absint( $request[ $base ] );
373 | }
374 |
375 | // Nav menu title.
376 | if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
377 | if ( is_string( $request['title'] ) ) {
378 | $prepared_nav_item['menu-item-title'] = $request['title'];
379 | } elseif ( ! empty( $request['title']['raw'] ) ) {
380 | $prepared_nav_item['menu-item-title'] = $request['title']['raw'];
381 | }
382 | }
383 |
384 | // Check if object id exists before saving.
385 | if ( ! $prepared_nav_item['menu-item-object'] ) {
386 | // If taxonony, check if term exists.
387 | if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) {
388 | $original = get_term( absint( $prepared_nav_item['menu-item-object-id'] ) );
389 | if ( empty( $original ) || is_wp_error( $original ) ) {
390 | return new WP_Error( 'rest_term_invalid_id', __( 'Invalid term ID.' ), array( 'status' => 400 ) );
391 | }
392 | $prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original );
393 |
394 | // If post, check if post object exists.
395 | } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) {
396 | $original = get_post( absint( $prepared_nav_item['menu-item-object-id'] ) );
397 | if ( empty( $original ) ) {
398 | return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 400 ) );
399 | }
400 | $prepared_nav_item['menu-item-object'] = get_post_type( $original );
401 | }
402 | }
403 |
404 | // If post type archive, check if post type exists.
405 | if ( 'post_type_archive' === $prepared_nav_item['menu-item-type'] ) {
406 | $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false;
407 | $original = get_post_type_object( $post_type );
408 | if ( empty( $original ) ) {
409 | return new WP_Error( 'rest_post_invalid_type', __( 'Invalid post type.' ), array( 'status' => 400 ) );
410 | }
411 | }
412 |
413 | // Check if menu item is type custom, then title and url are required.
414 | if ( 'custom' === $prepared_nav_item['menu-item-type'] ) {
415 | if ( '' === $prepared_nav_item['menu-item-title'] ) {
416 | return new WP_Error( 'rest_title_required', __( 'Title require if menu item of type custom.' ), array( 'status' => 400 ) );
417 | }
418 | if ( empty( $prepared_nav_item['menu-item-url'] ) ) {
419 | return new WP_Error( 'rest_url_required', __( 'URL require if menu item of type custom.' ), array( 'status' => 400 ) );
420 | }
421 | }
422 |
423 | // If menu id is set, valid the value of menu item position and parent id.
424 | if ( ! empty( $prepared_nav_item['menu-id'] ) ) {
425 | // Check if nav menu is valid.
426 | if ( ! is_nav_menu( $prepared_nav_item['menu-id'] ) ) {
427 | return new WP_Error( 'invalid_menu_id', __( 'Invalid menu ID.' ), array( 'status' => 400 ) );
428 | }
429 |
430 | // If menu item position is set to 0, insert as the last item in the existing menu.
431 | $menu_items = wp_get_nav_menu_items( $prepared_nav_item['menu-id'], array( 'post_status' => 'publish,draft' ) );
432 | if ( 0 === (int) $prepared_nav_item['menu-item-position'] ) {
433 | if ( $menu_items ) {
434 | $last_item = array_pop( $menu_items );
435 | if ( $last_item && isset( $last_item->menu_order ) ) {
436 | $prepared_nav_item['menu-item-position'] = $last_item->menu_order + 1;
437 | } else {
438 | $prepared_nav_item['menu-item-position'] = count( $menu_items );
439 | }
440 | } else {
441 | $prepared_nav_item['menu-item-position'] = 1;
442 | }
443 | }
444 |
445 | // Check if existing menu position is already in use by another menu item.
446 | $menu_item_ids = array();
447 | foreach ( $menu_items as $menu_item ) {
448 | $menu_item_ids[] = $menu_item->ID;
449 | if ( $menu_item->ID !== (int) $menu_item_db_id ) {
450 | if ( (int) $prepared_nav_item['menu-item-position'] === (int) $menu_item->menu_order ) {
451 | return new WP_Error( 'invalid_menu_order', __( 'Invalid menu position.' ), array( 'status' => 400 ) );
452 | }
453 | }
454 | }
455 |
456 | // Check if valid parent id is valid nav menu item in menu.
457 | if ( $prepared_nav_item['menu-item-parent-id'] ) {
458 | if ( ! is_nav_menu_item( $prepared_nav_item['menu-item-parent-id'] ) ) {
459 | return new WP_Error( 'invalid_menu_item_parent', __( 'Invalid menu item parent.' ), array( 'status' => 400 ) );
460 | }
461 | if ( ! $menu_item_ids || ! in_array( $prepared_nav_item['menu-item-parent-id'], $menu_item_ids, true ) ) {
462 | return new WP_Error( 'invalid_item_parent', __( 'Invalid menu item parent.' ), array( 'status' => 400 ) );
463 | }
464 | }
465 | }
466 |
467 | foreach ( array( 'menu-item-object-id', 'menu-item-parent-id' ) as $key ) {
468 | // Note we need to allow negative-integer IDs for previewed objects not inserted yet.
469 | $prepared_nav_item[ $key ] = intval( $prepared_nav_item[ $key ] );
470 | }
471 |
472 | foreach ( array( 'menu-item-type', 'menu-item-object', 'menu-item-target' ) as $key ) {
473 | $prepared_nav_item[ $key ] = sanitize_key( $prepared_nav_item[ $key ] );
474 | }
475 |
476 | // Valid xfn and classes are an array.
477 | foreach ( array( 'menu-item-xfn', 'menu-item-classes' ) as $key ) {
478 | $value = $prepared_nav_item[ $key ];
479 | if ( ! is_array( $value ) ) {
480 | $value = wp_parse_list( $value );
481 | }
482 | $prepared_nav_item[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) );
483 | }
484 |
485 | // Apply the same filters as when calling wp_insert_post().
486 |
487 | /** This filter is documented in wp-includes/post.php */
488 | $prepared_nav_item['menu-item-title'] = wp_unslash( apply_filters( 'title_save_pre', wp_slash( $prepared_nav_item['menu-item-title'] ) ) );
489 |
490 | /** This filter is documented in wp-includes/post.php */
491 | $prepared_nav_item['menu-item-attr-title'] = wp_unslash( apply_filters( 'excerpt_save_pre', wp_slash( $prepared_nav_item['menu-item-attr-title'] ) ) );
492 |
493 | /** This filter is documented in wp-includes/post.php */
494 | $prepared_nav_item['menu-item-description'] = wp_unslash( apply_filters( 'content_save_pre', wp_slash( $prepared_nav_item['menu-item-description'] ) ) );
495 |
496 | // Valid url.
497 | if ( '' !== $prepared_nav_item['menu-item-url'] ) {
498 | $prepared_nav_item['menu-item-url'] = esc_url_raw( $prepared_nav_item['menu-item-url'] );
499 | if ( '' === $prepared_nav_item['menu-item-url'] ) {
500 | // Fail sanitization if URL is invalid.
501 | return new WP_Error( 'invalid_url', __( 'Invalid URL.' ), array( 'status' => 400 ) );
502 | }
503 | }
504 | // Only draft / publish are valid post status for menu items.
505 | if ( 'publish' !== $prepared_nav_item['menu-item-status'] ) {
506 | $prepared_nav_item['menu-item-status'] = 'draft';
507 | }
508 |
509 | $prepared_nav_item = (object) $prepared_nav_item;
510 |
511 | /**
512 | * Filters a post before it is inserted via the REST API.
513 | *
514 | * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
515 | *
516 | * @param stdClass $prepared_post An object representing a single post prepared
517 | * for inserting or updating the database.
518 | * @param WP_REST_Request $request Request object.
519 | */
520 | return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_nav_item, $request );
521 | }
522 |
523 | /**
524 | * Prepares a single post output for response.
525 | *
526 | * @param object $post Post object.
527 | * @param WP_REST_Request $request Request object.
528 | *
529 | * @return WP_REST_Response Response object.
530 | */
531 | public function prepare_item_for_response( $post, $request ) {
532 | $fields = $this->get_fields_for_response( $request );
533 |
534 | // Base fields for every post.
535 | $menu_item = wp_setup_nav_menu_item( $post );
536 | $data = array();
537 | if ( in_array( 'id', $fields, true ) ) {
538 | $data['id'] = $menu_item->ID;
539 | }
540 |
541 | if ( in_array( 'title', $fields, true ) ) {
542 | add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
543 |
544 | $data['title'] = array(
545 | 'raw' => $menu_item->post_title,
546 | 'rendered' => $menu_item->title,
547 | );
548 |
549 | remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
550 | }
551 |
552 | if ( in_array( 'status', $fields, true ) ) {
553 | $data['status'] = $menu_item->post_status;
554 | }
555 |
556 | if ( in_array( 'url', $fields, true ) ) {
557 | $data['url'] = $menu_item->url;
558 | }
559 |
560 | if ( in_array( 'attr_title', $fields, true ) ) {
561 | // Same as post_excerpt.
562 | $data['attr_title'] = $menu_item->attr_title;
563 | }
564 |
565 | if ( in_array( 'description', $fields, true ) ) {
566 | // Same as post_content.
567 | $data['description'] = $menu_item->description;
568 | }
569 |
570 | if ( in_array( 'type', $fields, true ) ) {
571 | // Using 'item_type' since 'type' already exists.
572 | $data['type'] = $menu_item->type;
573 | }
574 |
575 | if ( in_array( 'type_label', $fields, true ) ) {
576 | // Using 'item_type_label' to match up with 'item_type' - IS READ ONLY!
577 | $data['type_label'] = $menu_item->type_label;
578 | }
579 |
580 | if ( in_array( 'object', $fields, true ) ) {
581 | $data['object'] = $menu_item->object;
582 | }
583 |
584 | if ( in_array( 'object_id', $fields, true ) ) {
585 | // Usually is a string, but lets expose as an integer.
586 | $data['object_id'] = absint( $menu_item->object_id );
587 | }
588 |
589 | if ( in_array( 'parent', $fields, true ) ) {
590 | // Same as post_parent, expose as integer.
591 | $data['parent'] = absint( $menu_item->menu_item_parent );
592 | }
593 |
594 | if ( in_array( 'menu_order', $fields, true ) ) {
595 | // Same as post_parent, expose as integer.
596 | $data['menu_order'] = absint( $menu_item->menu_order );
597 | }
598 |
599 | if ( in_array( 'menu_id', $fields, true ) ) {
600 | $data['menu_id'] = $this->get_menu_id( $menu_item->ID );
601 | }
602 |
603 | if ( in_array( 'target', $fields, true ) ) {
604 | $data['target'] = $menu_item->target;
605 | }
606 |
607 | if ( in_array( 'classes', $fields, true ) ) {
608 | $data['classes'] = (array) $menu_item->classes;
609 | }
610 |
611 | if ( in_array( 'xfn', $fields, true ) ) {
612 | $data['xfn'] = array_map( 'sanitize_html_class', explode( ' ', $menu_item->xfn ) );
613 | }
614 |
615 | if ( in_array( 'meta', $fields, true ) ) {
616 | $data['meta'] = $this->meta->get_value( $menu_item->ID, $request );
617 | }
618 |
619 | $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
620 |
621 | foreach ( $taxonomies as $taxonomy ) {
622 | $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
623 |
624 | if ( in_array( $base, $fields, true ) ) {
625 | $terms = get_the_terms( $post, $taxonomy->name );
626 | $data[ $base ] = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array();
627 | }
628 | }
629 |
630 | $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
631 | $data = $this->add_additional_fields_to_object( $data, $request );
632 | $data = $this->filter_response_by_context( $data, $context );
633 |
634 | // Wrap the data in a response object.
635 | $response = rest_ensure_response( $data );
636 |
637 | $links = $this->prepare_links( $menu_item );
638 | $response->add_links( $links );
639 |
640 | if ( ! empty( $links['self']['href'] ) ) {
641 | $actions = $this->get_available_actions( $menu_item, $request );
642 |
643 | $self = $links['self']['href'];
644 |
645 | foreach ( $actions as $rel ) {
646 | $response->add_link( $rel, $self );
647 | }
648 | }
649 |
650 | /**
651 | * Filters the post data for a response.
652 | *
653 | * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
654 | *
655 | * @param WP_REST_Response $response The response object.
656 | * @param object $post Post object.
657 | * @param WP_REST_Request $request Request object.
658 | */
659 | return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request );
660 | }
661 |
662 | /**
663 | * Prepares links for the request.
664 | *
665 | * @param object $menu_item Menu object.
666 | *
667 | * @return array Links for the given post.
668 | */
669 | protected function prepare_links( $menu_item ) {
670 | $links = parent::prepare_links( $menu_item );
671 |
672 | if ( 'post_type' === $menu_item->type && ! empty( $menu_item->object_id ) ) {
673 | $post_type_object = get_post_type_object( $menu_item->object );
674 | if ( $post_type_object->show_in_rest ) {
675 | $rest_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
676 | $url = rest_url( sprintf( 'wp/v2/%s/%d', $rest_base, $menu_item->object_id ) );
677 | $links['https://api.w.org/object'][] = array(
678 | 'href' => $url,
679 | 'post_type' => $menu_item->type,
680 | 'embeddable' => true,
681 | );
682 | }
683 | } elseif ( 'taxonomy' === $menu_item->type && ! empty( $menu_item->object_id ) ) {
684 | $taxonomy_object = get_taxonomy( $menu_item->object );
685 | if ( $taxonomy_object->show_in_rest ) {
686 | $rest_base = ! empty( $taxonomy_object->rest_base ) ? $taxonomy_object->rest_base : $taxonomy_object->name;
687 | $url = rest_url( sprintf( 'wp/v2/%s/%d', $rest_base, $menu_item->object_id ) );
688 | $links['https://api.w.org/object'][] = array(
689 | 'href' => $url,
690 | 'taxonomy' => $menu_item->type,
691 | 'embeddable' => true,
692 | );
693 | }
694 | }
695 |
696 | return $links;
697 | }
698 |
699 | /**
700 | * Retrieve Link Description Objects that should be added to the Schema for the posts collection.
701 | *
702 | * @return array
703 | */
704 | protected function get_schema_links() {
705 | $links = parent::get_schema_links();
706 | $href = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" );
707 | $links[] = array(
708 | 'rel' => 'https://api.w.org/object',
709 | 'title' => __( 'Get linked object.' ),
710 | 'href' => $href,
711 | 'targetSchema' => array(
712 | 'type' => 'object',
713 | 'properties' => array(
714 | 'object' => array(
715 | 'type' => 'integer',
716 | ),
717 | ),
718 | ),
719 | );
720 |
721 | return $links;
722 | }
723 |
724 | /**
725 | * Retrieves the term's schema, conforming to JSON Schema.
726 | *
727 | * @return array Item schema data.
728 | */
729 | public function get_item_schema() {
730 | $schema = array(
731 | '$schema' => 'http://json-schema.org/draft-04/schema#',
732 | 'title' => $this->post_type,
733 | 'type' => 'object',
734 | );
735 |
736 | $schema['properties']['title'] = array(
737 | 'description' => __( 'The title for the object.' ),
738 | 'type' => 'object',
739 | 'context' => array( 'view', 'edit', 'embed' ),
740 | 'arg_options' => array(
741 | // Note: sanitization implemented in self::prepare_item_for_database().
742 | 'sanitize_callback' => null,
743 | // Note: validation implemented in self::prepare_item_for_database().
744 | 'validate_callback' => null,
745 | ),
746 | 'properties' => array(
747 | 'raw' => array(
748 | 'description' => __( 'Title for the object, as it exists in the database.' ),
749 | 'type' => 'string',
750 | 'context' => array( 'edit' ),
751 | ),
752 | 'rendered' => array(
753 | 'description' => __( 'HTML title for the object, transformed for display.' ),
754 | 'type' => 'string',
755 | 'context' => array( 'view', 'edit', 'embed' ),
756 | 'readonly' => true,
757 | ),
758 | ),
759 | );
760 |
761 | $schema['properties']['id'] = array(
762 | 'description' => __( 'Unique identifier for the object.' ),
763 | 'type' => 'integer',
764 | 'default' => 0,
765 | 'minimum' => 0,
766 | 'context' => array( 'view', 'edit', 'embed' ),
767 | 'readonly' => true,
768 | );
769 |
770 | $schema['properties']['type_label'] = array(
771 | 'description' => __( 'Name of type.' ),
772 | 'type' => 'string',
773 | 'context' => array( 'view', 'edit', 'embed' ),
774 | 'readonly' => true,
775 | );
776 |
777 | $schema['properties']['type'] = array(
778 | 'description' => __( 'The family of objects originally represented, such as "post_type" or "taxonomy".' ),
779 | 'type' => 'string',
780 | 'enum' => array( 'taxonomy', 'post_type', 'post_type_archive', 'custom' ),
781 | 'context' => array( 'view', 'edit', 'embed' ),
782 | 'default' => 'custom',
783 | );
784 |
785 | $schema['properties']['status'] = array(
786 | 'description' => __( 'A named status for the object.' ),
787 | 'type' => 'string',
788 | 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ),
789 | 'default' => 'publish',
790 | 'context' => array( 'view', 'edit', 'embed' ),
791 | );
792 |
793 | $schema['properties']['parent'] = array(
794 | 'description' => __( 'The ID for the parent of the object.' ),
795 | 'type' => 'integer',
796 | 'minimum' => 0,
797 | 'default' => 0,
798 | 'context' => array( 'view', 'edit', 'embed' ),
799 | );
800 |
801 | $schema['properties']['attr_title'] = array(
802 | 'description' => __( 'Text for the title attribute of the link element for this menu item.' ),
803 | 'type' => 'string',
804 | 'context' => array( 'view', 'edit', 'embed' ),
805 | 'arg_options' => array(
806 | 'sanitize_callback' => 'sanitize_text_field',
807 | ),
808 | );
809 |
810 | $schema['properties']['classes'] = array(
811 | 'description' => __( 'Class names for the link element of this menu item.' ),
812 | 'type' => 'array',
813 | 'items' => array(
814 | 'type' => 'string',
815 | ),
816 | 'context' => array( 'view', 'edit', 'embed' ),
817 | 'arg_options' => array(
818 | 'sanitize_callback' => function ( $value ) {
819 | return array_map( 'sanitize_html_class', wp_parse_list( $value ) );
820 | },
821 | ),
822 | );
823 |
824 | $schema['properties']['description'] = array(
825 | 'description' => __( 'The description of this menu item.' ),
826 | 'type' => 'string',
827 | 'context' => array( 'view', 'edit', 'embed' ),
828 | 'arg_options' => array(
829 | 'sanitize_callback' => 'sanitize_text_field',
830 | ),
831 | );
832 |
833 | $schema['properties']['menu_order'] = array(
834 | 'description' => __( 'The DB ID of the nav_menu_item that is this item\'s menu parent, if any . 0 otherwise . ' ),
835 | 'context' => array( 'view', 'edit', 'embed' ),
836 | 'type' => 'integer',
837 | 'minimum' => 0,
838 | 'default' => 0,
839 | );
840 | $schema['properties']['object'] = array(
841 | 'description' => __( 'The type of object originally represented, such as "category," "post", or "attachment."' ),
842 | 'context' => array( 'view', 'edit', 'embed' ),
843 | 'type' => 'string',
844 | );
845 |
846 | $schema['properties']['object_id'] = array(
847 | 'description' => __( 'The DB ID of the original object this menu item represents, e . g . ID for posts and term_id for categories .' ),
848 | 'context' => array( 'view', 'edit', 'embed' ),
849 | 'type' => 'integer',
850 | 'minimum' => 0,
851 | 'default' => 0,
852 | );
853 |
854 | $schema['properties']['target'] = array(
855 | 'description' => __( 'The target attribute of the link element for this menu item.' ),
856 | 'type' => 'string',
857 | 'context' => array( 'view', 'edit', 'embed' ),
858 | 'enum' => array(
859 | '_blank',
860 | '',
861 | ),
862 | );
863 |
864 | $schema['properties']['type_label'] = array(
865 | 'description' => __( 'The singular label used to describe this type of menu item.' ),
866 | 'context' => array( 'view', 'edit', 'embed' ),
867 | 'type' => 'string',
868 | 'readonly' => true,
869 | );
870 |
871 | $schema['properties']['url'] = array(
872 | 'description' => __( 'The URL to which this menu item points.' ),
873 | 'type' => 'string',
874 | 'format' => 'uri',
875 | 'context' => array( 'view', 'edit', 'embed' ),
876 | );
877 |
878 | $schema['properties']['xfn'] = array(
879 | 'description' => __( 'The XFN relationship expressed in the link of this menu item.' ),
880 | 'type' => 'array',
881 | 'items' => array(
882 | 'type' => 'string',
883 | ),
884 | 'context' => array( 'view', 'edit', 'embed' ),
885 | 'arg_options' => array(
886 | 'sanitize_callback' => function ( $value ) {
887 | return array_map( 'sanitize_html_class', wp_parse_list( $value ) );
888 | },
889 | ),
890 | );
891 |
892 | $schema['properties']['_invalid'] = array(
893 | 'description' => __( 'Whether the menu item represents an object that no longer exists .' ),
894 | 'context' => array( 'view', 'edit', 'embed' ),
895 | 'type' => 'boolean',
896 | 'readonly' => true,
897 | );
898 |
899 | $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
900 |
901 | foreach ( $taxonomies as $taxonomy ) {
902 | $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
903 | $schema['properties'][ $base ] = array(
904 | /* translators: %s: taxonomy name */
905 | 'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.' ), $taxonomy->name ),
906 | 'type' => 'array',
907 | 'items' => array(
908 | 'type' => 'integer',
909 | ),
910 | 'context' => array( 'view', 'edit' ),
911 | );
912 |
913 | if ( 'nav_menu' === $taxonomy->name ) {
914 | $schema['properties'][ $base ]['type'] = 'integer';
915 | unset( $schema['properties'][ $base ]['items'] );
916 | }
917 | }
918 |
919 | $schema['properties']['meta'] = $this->meta->get_field_schema();
920 |
921 | $schema_links = $this->get_schema_links();
922 |
923 | if ( $schema_links ) {
924 | $schema['links'] = $schema_links;
925 | }
926 |
927 | return $this->add_additional_fields_schema( $schema );
928 | }
929 |
930 | /**
931 | * Retrieves the query params for the posts collection.
932 | *
933 | * @return array Collection parameters.
934 | */
935 | public function get_collection_params() {
936 | $query_params = parent::get_collection_params();
937 |
938 | $query_params['menu_order'] = array(
939 | 'description' => __( 'Limit result set to posts with a specific menu_order value.' ),
940 | 'type' => 'integer',
941 | );
942 |
943 | $query_params['order'] = array(
944 | 'description' => __( 'Order sort attribute ascending or descending.' ),
945 | 'type' => 'string',
946 | 'default' => 'asc',
947 | 'enum' => array( 'asc', 'desc' ),
948 | );
949 |
950 | $query_params['orderby'] = array(
951 | 'description' => __( 'Sort collection by object attribute.' ),
952 | 'type' => 'string',
953 | 'default' => 'menu_order',
954 | 'enum' => array(
955 | 'author',
956 | 'date',
957 | 'id',
958 | 'include',
959 | 'modified',
960 | 'parent',
961 | 'relevance',
962 | 'slug',
963 | 'include_slugs',
964 | 'title',
965 | 'menu_order',
966 | ),
967 | );
968 | // Change dfault to 100 items.
969 | $query_params['per_page']['default'] = 100;
970 |
971 | return $query_params;
972 | }
973 |
974 | /**
975 | * Determines the allowed query_vars for a get_items() response and prepares
976 | * them for WP_Query.
977 | *
978 | * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array.
979 | * @param WP_REST_Request $request Optional. Full details about the request.
980 | *
981 | * @return array Items query arguments.
982 | */
983 | protected function prepare_items_query( $prepared_args = array(), $request = null ) {
984 | $query_args = parent::prepare_items_query( $prepared_args, $request );
985 |
986 | // Map to proper WP_Query orderby param.
987 | if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) {
988 | $orderby_mappings = array(
989 | 'id' => 'ID',
990 | 'include' => 'post__in',
991 | 'slug' => 'post_name',
992 | 'include_slugs' => 'post_name__in',
993 | 'menu_order' => 'menu_order',
994 | );
995 |
996 | if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
997 | $query_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
998 | }
999 | }
1000 |
1001 | return $query_args;
1002 | }
1003 |
1004 | /**
1005 | * Checks whether current user can assign all terms sent with the current request.
1006 | *
1007 | * @param WP_REST_Request $request The request object with post and terms data.
1008 | *
1009 | * @return bool Whether the current user can assign the provided terms.
1010 | */
1011 | protected function check_assign_terms_permission( $request ) {
1012 | $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
1013 | foreach ( $taxonomies as $taxonomy ) {
1014 | $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
1015 |
1016 | if ( ! isset( $request[ $base ] ) ) {
1017 | continue;
1018 | }
1019 |
1020 | foreach ( (array) $request[ $base ] as $term_id ) {
1021 | if ( ! $term_id ) {
1022 | continue;
1023 | }
1024 |
1025 | // Invalid terms will be rejected later.
1026 | if ( ! get_term( $term_id, $taxonomy->name ) ) {
1027 | continue;
1028 | };
1029 |
1030 | if ( ! current_user_can( 'assign_term', (int) $term_id ) ) {
1031 | return false;
1032 | }
1033 | }
1034 | }
1035 |
1036 | return true;
1037 | }
1038 |
1039 | /**
1040 | * Get menu id of current menu item.
1041 | *
1042 | * @param int $menu_item_id Menu item id.
1043 | *
1044 | * @return int
1045 | */
1046 | protected function get_menu_id( $menu_item_id ) {
1047 | $menu_ids = wp_get_post_terms( $menu_item_id, 'nav_menu', array( 'fields' => 'ids' ) );
1048 | $menu_id = 0;
1049 | if ( $menu_ids && ! is_wp_error( $menu_ids ) ) {
1050 | $menu_id = array_shift( $menu_ids );
1051 | }
1052 |
1053 | return $menu_id;
1054 | }
1055 | }
1056 |
--------------------------------------------------------------------------------
/lib/class-wp-rest-menu-locations-controller.php:
--------------------------------------------------------------------------------
1 | namespace = 'wp/v2';
21 | $this->rest_base = 'menu-locations';
22 | }
23 |
24 | /**
25 | * Registers the routes for the objects of the controller.
26 | *
27 | * @see register_rest_route()
28 | */
29 | public function register_routes() {
30 | register_rest_route(
31 | $this->namespace,
32 | '/' . $this->rest_base,
33 | array(
34 | array(
35 | 'methods' => WP_REST_Server::READABLE,
36 | 'callback' => array( $this, 'get_items' ),
37 | 'permission_callback' => array( $this, 'get_items_permissions_check' ),
38 | 'args' => $this->get_collection_params(),
39 | ),
40 | 'schema' => array( $this, 'get_public_item_schema' ),
41 | )
42 | );
43 |
44 | register_rest_route(
45 | $this->namespace,
46 | '/' . $this->rest_base . '/(?P[\w-]+)',
47 | array(
48 | 'args' => array(
49 | 'location' => array(
50 | 'description' => __( 'An alphanumeric identifier for the menu location.' ),
51 | 'type' => 'string',
52 | ),
53 | ),
54 | array(
55 | 'methods' => WP_REST_Server::READABLE,
56 | 'callback' => array( $this, 'get_item' ),
57 | 'permission_callback' => array( $this, 'get_item_permissions_check' ),
58 | 'args' => array(
59 | 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
60 | ),
61 | ),
62 | 'schema' => array( $this, 'get_public_item_schema' ),
63 | )
64 | );
65 | }
66 |
67 | /**
68 | * Checks whether a given request has permission to read menu locations.
69 | *
70 | * @param WP_REST_Request $request Full details about the request.
71 | *
72 | * @return WP_Error|bool True if the request has read access, WP_Error object otherwise.
73 | */
74 | public function get_items_permissions_check( $request ) {
75 | if ( ! current_user_can( 'edit_theme_options' ) ) {
76 | return new WP_Error( 'rest_cannot_view', __( 'Sorry, you are not allowed to view menu locations.' ), array( 'status' => rest_authorization_required_code() ) );
77 | }
78 |
79 | return true;
80 | }
81 |
82 | /**
83 | * Retrieves all menu locations, depending on user context.
84 | *
85 | * @param WP_REST_Request $request Full details about the request.
86 | *
87 | * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
88 | */
89 | public function get_items( $request ) {
90 | $data = array();
91 |
92 | foreach ( get_registered_nav_menus() as $name => $description ) {
93 | $location = new stdClass();
94 | $location->name = $name;
95 | $location->description = $description;
96 |
97 | $location = $this->prepare_item_for_response( $location, $request );
98 | $data[ $name ] = $this->prepare_response_for_collection( $location );
99 | }
100 |
101 | return rest_ensure_response( $data );
102 | }
103 |
104 | /**
105 | * Checks if a given request has access to read a menu location.
106 | *
107 | * @param WP_REST_Request $request Full details about the request.
108 | *
109 | * @return WP_Error|bool True if the request has read access for the item, WP_Error object otherwise.
110 | */
111 | public function get_item_permissions_check( $request ) {
112 | if ( ! current_user_can( 'edit_theme_options' ) ) {
113 | return new WP_Error( 'rest_cannot_view', __( 'Sorry, you are not allowed to view menu locations.' ), array( 'status' => rest_authorization_required_code() ) );
114 | }
115 | if ( ! array_key_exists( $request['location'], get_registered_nav_menus() ) ) {
116 | return new WP_Error( 'rest_menu_location_invalid', __( 'Invalid menu location.' ), array( 'status' => 404 ) );
117 | }
118 |
119 | return true;
120 | }
121 |
122 | /**
123 | * Retrieves a specific menu location.
124 | *
125 | * @param WP_REST_Request $request Full details about the request.
126 | *
127 | * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
128 | */
129 | public function get_item( $request ) {
130 | $registered_menus = get_registered_nav_menus();
131 | if ( ! array_key_exists( $request['location'], $registered_menus ) ) {
132 | return new WP_Error( 'rest_menu_location_invalid', __( 'Invalid menu location.' ), array( 'status' => 404 ) );
133 | }
134 |
135 | $location = new stdClass();
136 | $location->name = $request['location'];
137 | $location->description = $registered_menus[ $location->name ];
138 |
139 | $data = $this->prepare_item_for_response( $location, $request );
140 |
141 | return rest_ensure_response( $data );
142 | }
143 |
144 | /**
145 | * Prepares a menu location object for serialization.
146 | *
147 | * @param stdClass $location Post status data.
148 | * @param WP_REST_Request $request Full details about the request.
149 | *
150 | * @return WP_REST_Response Post status data.
151 | */
152 | public function prepare_item_for_response( $location, $request ) {
153 | $locations = get_nav_menu_locations();
154 | $menu = ( isset( $locations[ $location->name ] ) ) ? $locations[ $location->name ] : 0;
155 | $data = array(
156 | 'name' => $location->name,
157 | 'description' => $location->description,
158 | 'menu' => $menu,
159 | );
160 |
161 | $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
162 | $data = $this->add_additional_fields_to_object( $data, $request );
163 | $data = $this->filter_response_by_context( $data, $context );
164 |
165 | $response = rest_ensure_response( $data );
166 |
167 | $response->add_links( $this->prepare_links( $location ) );
168 |
169 | /**
170 | * Filters a menu location returned from the REST API.
171 | *
172 | * Allows modification of the menu location data right before it is
173 | * returned.
174 | *
175 | * @param WP_REST_Response $response The response object.
176 | * @param object $location The original status object.
177 | * @param WP_REST_Request $request Request used to generate the response.
178 | */
179 | return apply_filters( 'rest_prepare_menu_location', $response, $location, $request );
180 | }
181 |
182 | /**
183 | * Retrieves the menu location's schema, conforming to JSON Schema.
184 | *
185 | * @return array Item schema data.
186 | */
187 | public function get_item_schema() {
188 | $schema = array(
189 | '$schema' => 'http://json-schema.org/draft-04/schema#',
190 | 'title' => 'menu-location',
191 | 'type' => 'object',
192 | 'properties' => array(
193 | 'name' => array(
194 | 'description' => __( 'The name of the menu location.' ),
195 | 'type' => 'string',
196 | 'context' => array( 'embed', 'view', 'edit' ),
197 | 'readonly' => true,
198 | ),
199 | 'description' => array(
200 | 'description' => __( 'The description of the menu location.' ),
201 | 'type' => 'string',
202 | 'context' => array( 'embed', 'view', 'edit' ),
203 | 'readonly' => true,
204 | ),
205 | 'menu' => array(
206 | 'description' => __( 'The ID of the assigned menu.' ),
207 | 'type' => 'integer',
208 | 'context' => array( 'embed', 'view', 'edit' ),
209 | 'readonly' => true,
210 | ),
211 | ),
212 | );
213 |
214 | return $this->add_additional_fields_schema( $schema );
215 | }
216 |
217 | /**
218 | * Retrieves the query params for collections.
219 | *
220 | * @return array Collection parameters.
221 | */
222 | public function get_collection_params() {
223 | return array(
224 | 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
225 | );
226 | }
227 |
228 | /**
229 | * Prepares links for the request.
230 | *
231 | * @param stdClass $location Menu location.
232 | *
233 | * @return array Links for the given menu location.
234 | */
235 | protected function prepare_links( $location ) {
236 | $base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
237 |
238 | // Entity meta.
239 | $links = array(
240 | 'self' => array(
241 | 'href' => rest_url( trailingslashit( $base ) . $location->name ),
242 | ),
243 | 'collection' => array(
244 | 'href' => rest_url( $base ),
245 | ),
246 | );
247 |
248 | $locations = get_nav_menu_locations();
249 | $menu = ( isset( $locations[ $location->name ] ) ) ? $locations[ $location->name ] : 0;
250 | if ( $menu ) {
251 | $taxonomy_object = get_taxonomy( 'nav_menu' );
252 | if ( $taxonomy_object->show_in_rest ) {
253 | $rest_base = ! empty( $taxonomy_object->rest_base ) ? $taxonomy_object->rest_base : $taxonomy_object->name;
254 | $url = rest_url( sprintf( 'wp/v2/%s/%d', $rest_base, $menu ) );
255 | $links['https://api.w.org/menu'][] = array(
256 | 'href' => $url,
257 | 'embeddable' => true,
258 | );
259 | }
260 | }
261 |
262 | return $links;
263 | }
264 | }
265 |
--------------------------------------------------------------------------------
/lib/class-wp-rest-menus-controller.php:
--------------------------------------------------------------------------------
1 | taxonomy );
24 | if ( ! $tax_obj || ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
25 | return false;
26 | }
27 | if ( ! current_user_can( $tax_obj->cap->edit_terms ) ) {
28 | if ( 'edit' === $request['context'] ) {
29 | return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit terms in this taxonomy.' ), array( 'status' => rest_authorization_required_code() ) );
30 | }
31 | return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view these menus, unless you have access to permission edit them. ' ), array( 'status' => rest_authorization_required_code() ) );
32 | }
33 | return true;
34 | }
35 |
36 | /**
37 | * Checks if a request has access to read or edit the specified menu.
38 | *
39 | * @param WP_REST_Request $request Full details about the request.
40 | * @return bool|WP_Error True if the request has read access for the item, otherwise false or WP_Error object.
41 | */
42 | public function get_item_permissions_check( $request ) {
43 | $term = $this->get_term( $request['id'] );
44 | if ( is_wp_error( $term ) ) {
45 | return $term;
46 | }
47 | if ( ! current_user_can( 'edit_term', $term->term_id ) ) {
48 | if ( 'edit' === $request['context'] ) {
49 | return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit this term.' ), array( 'status' => rest_authorization_required_code() ) );
50 | }
51 | return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this menu, unless you have access to permission edit it. ' ), array( 'status' => rest_authorization_required_code() ) );
52 | }
53 | return true;
54 | }
55 |
56 | /**
57 | * Get the term, if the ID is valid.
58 | *
59 | * @param int $id Supplied ID.
60 | *
61 | * @return WP_Term|WP_Error Term object if ID is valid, WP_Error otherwise.
62 | */
63 | protected function get_term( $id ) {
64 | $term = parent::get_term( $id );
65 |
66 | if ( is_wp_error( $term ) ) {
67 | return $term;
68 | }
69 |
70 | $nav_term = wp_get_nav_menu_object( $term );
71 |
72 | return $nav_term;
73 | }
74 |
75 | /**
76 | * Checks if a request has access to create a term.
77 | * Also check if request can assign menu locations.
78 | *
79 | * @param WP_REST_Request $request Full details about the request.
80 | *
81 | * @return bool|WP_Error True if the request has access to create items, false or WP_Error object otherwise.
82 | */
83 | public function create_item_permissions_check( $request ) {
84 | $check = $this->check_assign_locations_permission( $request );
85 | if ( is_wp_error( $check ) ) {
86 | return $check;
87 | }
88 |
89 | return parent::create_item_permissions_check( $request );
90 | }
91 |
92 | /**
93 | * Checks if a request has access to update the specified term.
94 | *
95 | * @param WP_REST_Request $request Full details about the request.
96 | *
97 | * @return bool|WP_Error True if the request has access to update the item, false or WP_Error object otherwise.
98 | */
99 | public function update_item_permissions_check( $request ) {
100 | $check = $this->check_assign_locations_permission( $request );
101 | if ( is_wp_error( $check ) ) {
102 | return $check;
103 | }
104 |
105 | return parent::update_item_permissions_check( $request );
106 | }
107 |
108 | /**
109 | * Checks whether current user can assign all locations sent with the current request.
110 | *
111 | * @param WP_REST_Request $request The request object with post and locations data.
112 | *
113 | * @return bool Whether the current user can assign the provided terms.
114 | */
115 | protected function check_assign_locations_permission( $request ) {
116 | if ( ! isset( $request['locations'] ) ) {
117 | return true;
118 | }
119 |
120 | if ( ! current_user_can( 'edit_theme_options' ) ) {
121 | return new WP_Error( 'rest_cannot_assign_location', __( 'Sorry, you are not allowed to assign the provided locations.' ), array( 'status' => rest_authorization_required_code() ) );
122 | }
123 |
124 | foreach ( $request['locations'] as $location ) {
125 | if ( ! array_key_exists( $location, get_registered_nav_menus() ) ) {
126 | return new WP_Error(
127 | 'rest_menu_location_invalid',
128 | __( 'Invalid menu location.' ),
129 | array(
130 | 'status' => 400,
131 | 'location' => $location,
132 | )
133 | );
134 | }
135 | }
136 |
137 | return true;
138 | }
139 |
140 | /**
141 | * Prepares a single term output for response.
142 | *
143 | * @param obj $term Term object.
144 | * @param WP_REST_Request $request Request object.
145 | *
146 | * @return WP_REST_Response $response Response object.
147 | */
148 | public function prepare_item_for_response( $term, $request ) {
149 | $nav_menu = wp_get_nav_menu_object( $term );
150 |
151 | return parent::prepare_item_for_response( $nav_menu, $request );
152 | }
153 |
154 | /**
155 | * Prepares links for the request.
156 | *
157 | * @param object $term Term object.
158 | *
159 | * @return array Links for the given term.
160 | */
161 | protected function prepare_links( $term ) {
162 | $links = parent::prepare_links( $term );
163 |
164 | $locations = get_nav_menu_locations();
165 | $rest_base = 'menu-locations';
166 | foreach ( $locations as $menu_name => $menu_id ) {
167 | if ( $term->term_id === $menu_id ) {
168 | $url = rest_url( sprintf( 'wp/v2/%s/%s', $rest_base, $menu_name ) );
169 | $links['https://api.w.org/menu-location'][] = array(
170 | 'href' => $url,
171 | 'embeddable' => true,
172 | );
173 | }
174 | }
175 |
176 | return $links;
177 | }
178 |
179 | /**
180 | * Prepares a single term for create or update.
181 | *
182 | * @param WP_REST_Request $request Request object.
183 | *
184 | * @return array $prepared_term Term object.
185 | */
186 | public function prepare_item_for_database( $request ) {
187 | $prepared_term = parent::prepare_item_for_database( $request );
188 |
189 | $prepared_term = (array) $prepared_term;
190 | $schema = $this->get_item_schema();
191 | if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
192 | $prepared_term['menu-name'] = $request['name'];
193 | }
194 |
195 | return $prepared_term;
196 | }
197 |
198 | /**
199 | * Creates a single term in a taxonomy.
200 | *
201 | * @param WP_REST_Request $request Full details about the request.
202 | *
203 | * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
204 | */
205 | public function create_item( $request ) {
206 | if ( isset( $request['parent'] ) ) {
207 | if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
208 | return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Cannot set parent term, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
209 | }
210 |
211 | $parent = wp_get_nav_menu_object( (int) $request['parent'] );
212 |
213 | if ( ! $parent ) {
214 | return new WP_Error( 'rest_term_invalid', __( 'Parent term does not exist.' ), array( 'status' => 400 ) );
215 | }
216 | }
217 |
218 | $prepared_term = $this->prepare_item_for_database( $request );
219 |
220 | $term = wp_update_nav_menu_object( 0, wp_slash( (array) $prepared_term ) );
221 |
222 | if ( is_wp_error( $term ) ) {
223 | /*
224 | * If we're going to inform the client that the term already exists,
225 | * give them the identifier for future use.
226 | */
227 | $term_id = $term->get_error_data( 'term_exists' );
228 | if ( $term_id ) {
229 | $existing_term = get_term( $term_id, $this->taxonomy );
230 | $term->add_data( $existing_term->term_id, 'term_exists' );
231 | $term->add_data(
232 | array(
233 | 'status' => 400,
234 | 'term_id' => $term_id,
235 | )
236 | );
237 | }
238 |
239 | return $term;
240 | }
241 |
242 | $term = $this->get_term( $term );
243 |
244 | /**
245 | * Fires after a single term is created or updated via the REST API.
246 | *
247 | * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
248 | *
249 | * @param WP_Term $term Inserted or updated term object.
250 | * @param WP_REST_Request $request Request object.
251 | * @param bool $creating True when creating a term, false when updating.
252 | */
253 | do_action( "rest_insert_{$this->taxonomy}", $term, $request, true );
254 |
255 | $schema = $this->get_item_schema();
256 | if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
257 | $meta_update = $this->meta->update_value( $request['meta'], $term->term_id );
258 |
259 | if ( is_wp_error( $meta_update ) ) {
260 | return $meta_update;
261 | }
262 | }
263 |
264 | $locations_update = $this->handle_locations( $term->term_id, $request );
265 |
266 | if ( is_wp_error( $locations_update ) ) {
267 | return $locations_update;
268 | }
269 |
270 | $fields_update = $this->update_additional_fields_for_object( $term, $request );
271 |
272 | if ( is_wp_error( $fields_update ) ) {
273 | return $fields_update;
274 | }
275 |
276 | $request->set_param( 'context', 'view' );
277 |
278 | /**
279 | * Fires after a single term is completely created or updated via the REST API.
280 | *
281 | * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
282 | *
283 | * @param WP_Term $term Inserted or updated term object.
284 | * @param WP_REST_Request $request Request object.
285 | * @param bool $creating True when creating a term, false when updating.
286 | *
287 | * @since 5.0.0
288 | */
289 | do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, true );
290 |
291 | $response = $this->prepare_item_for_response( $term, $request );
292 | $response = rest_ensure_response( $response );
293 |
294 | $response->set_status( 201 );
295 | $response->header( 'Location', rest_url( $this->namespace . '/' . $this->rest_base . '/' . $term->term_id ) );
296 |
297 | return $response;
298 | }
299 |
300 | /**
301 | * Updates a single term from a taxonomy.
302 | *
303 | * @param WP_REST_Request $request Full details about the request.
304 | *
305 | * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
306 | */
307 | public function update_item( $request ) {
308 | $term = $this->get_term( $request['id'] );
309 | if ( is_wp_error( $term ) ) {
310 | return $term;
311 | }
312 |
313 | if ( isset( $request['parent'] ) ) {
314 | if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
315 | return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Cannot set parent term, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
316 | }
317 |
318 | $parent = get_term( (int) $request['parent'], $this->taxonomy );
319 |
320 | if ( ! $parent ) {
321 | return new WP_Error( 'rest_term_invalid', __( 'Parent term does not exist.' ), array( 'status' => 400 ) );
322 | }
323 | }
324 |
325 | $prepared_term = $this->prepare_item_for_database( $request );
326 |
327 | // Only update the term if we haz something to update.
328 | if ( ! empty( $prepared_term ) ) {
329 | $update = wp_update_nav_menu_object( $term->term_id, wp_slash( (array) $prepared_term ) );
330 |
331 | if ( is_wp_error( $update ) ) {
332 | return $update;
333 | }
334 | }
335 |
336 | $term = get_term( $term->term_id, $this->taxonomy );
337 |
338 | /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
339 | do_action( "rest_insert_{$this->taxonomy}", $term, $request, false );
340 |
341 | $schema = $this->get_item_schema();
342 | if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
343 | $meta_update = $this->meta->update_value( $request['meta'], $term->term_id );
344 |
345 | if ( is_wp_error( $meta_update ) ) {
346 | return $meta_update;
347 | }
348 | }
349 |
350 | $locations_update = $this->handle_locations( $term->term_id, $request );
351 |
352 | if ( is_wp_error( $locations_update ) ) {
353 | return $locations_update;
354 | }
355 |
356 | $fields_update = $this->update_additional_fields_for_object( $term, $request );
357 |
358 | if ( is_wp_error( $fields_update ) ) {
359 | return $fields_update;
360 | }
361 |
362 | $request->set_param( 'context', 'view' );
363 |
364 | /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
365 | do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, false );
366 |
367 | $response = $this->prepare_item_for_response( $term, $request );
368 |
369 | return rest_ensure_response( $response );
370 | }
371 |
372 | /**
373 | * Deletes a single term from a taxonomy.
374 | *
375 | * @param WP_REST_Request $request Full details about the request.
376 | *
377 | * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
378 | */
379 | public function delete_item( $request ) {
380 | $term = $this->get_term( $request['id'] );
381 | if ( is_wp_error( $term ) ) {
382 | return $term;
383 | }
384 |
385 | $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
386 |
387 | // We don't support trashing for terms.
388 | if ( ! $force ) {
389 | /* translators: %s: force=true */
390 | return new WP_Error( 'rest_trash_not_supported', sprintf( __( "Terms do not support trashing. Set '%s' to delete." ), 'force=true' ), array( 'status' => 501 ) );
391 | }
392 |
393 | $request->set_param( 'context', 'view' );
394 |
395 | $previous = $this->prepare_item_for_response( $term, $request );
396 |
397 | $retval = wp_delete_nav_menu( $term );
398 |
399 | if ( ! $retval ) {
400 | return new WP_Error( 'rest_cannot_delete', __( 'The term cannot be deleted.' ), array( 'status' => 500 ) );
401 | }
402 |
403 | $response = new WP_REST_Response();
404 | $response->set_data(
405 | array(
406 | 'deleted' => true,
407 | 'previous' => $previous->get_data(),
408 | )
409 | );
410 |
411 | /**
412 | * Fires after a single term is deleted via the REST API.
413 | *
414 | * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
415 | *
416 | * @param WP_Term $term The deleted term.
417 | * @param WP_REST_Response $response The response data.
418 | * @param WP_REST_Request $request The request sent to the API.
419 | */
420 | do_action( "rest_delete_{$this->taxonomy}", $term, $response, $request );
421 |
422 | return $response;
423 | }
424 |
425 | /**
426 | * Updates the menu's locations from a REST request.
427 | *
428 | * @param int $menu_id The menu id to update the location form.
429 | * @param WP_REST_Request $request The request object with menu and locations data.
430 | *
431 | * @return true|WP_Error WP_Error on an error assigning any of the locations, otherwise null.
432 | */
433 | protected function handle_locations( $menu_id, $request ) {
434 | if ( ! isset( $request['locations'] ) ) {
435 | return true;
436 | }
437 |
438 | $menu_locations = get_registered_nav_menus();
439 | $menu_locations = array_keys( $menu_locations );
440 | $new_locations = array();
441 | foreach ( $request['locations'] as $location ) {
442 | if ( ! in_array( $location, $menu_locations, true ) ) {
443 | return new WP_Error( 'invalid_menu_location', __( 'Menu location does not exist.' ), array( 'status' => 400 ) );
444 | }
445 | $new_locations[ $location ] = $menu_id;
446 | }
447 | $assigned_menu = get_nav_menu_locations();
448 | foreach ( $assigned_menu as $location => $term_id ) {
449 | if ( $term_id === $menu_id ) {
450 | unset( $assigned_menu[ $location ] );
451 | }
452 | }
453 | $new_assignments = array_merge( $assigned_menu, $new_locations );
454 | set_theme_mod( 'nav_menu_locations', $new_assignments );
455 |
456 | return true;
457 | }
458 |
459 | /**
460 | * Retrieves the term's schema, conforming to JSON Schema.
461 | *
462 | * @return array Item schema data.
463 | */
464 | public function get_item_schema() {
465 | $schema = parent::get_item_schema();
466 | unset( $schema['properties']['count'] );
467 | unset( $schema['properties']['link'] );
468 | unset( $schema['properties']['taxonomy'] );
469 |
470 | $schema['properties']['locations'] = array(
471 | 'description' => __( 'The locations assigned to the menu.' ),
472 | 'type' => 'array',
473 | 'items' => array(
474 | 'type' => 'string',
475 | ),
476 | 'context' => array( 'view', 'edit' ),
477 | );
478 |
479 | return $schema;
480 | }
481 | }
482 |
--------------------------------------------------------------------------------
/multisite.xml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | tests
16 |
17 |
18 |
19 |
20 |
21 | .
22 |
23 |
24 | ./lib
25 | ./plugin.php
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wp-api-menus-endpoints",
3 | "version": "0.1.0",
4 | "license" : "GPL-2.0+",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/WP-API/menus-endpoints.git"
8 | },
9 | "main": "Gruntfile.js",
10 | "author": "WP-API Team ",
11 | "devDependencies": {
12 | "grunt": "^0.4.5",
13 | "grunt-phpcs": "^0.4.0",
14 | "phplint": "^1.6.1"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 | ./tests/
13 | ./tests/test-sample.php
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/plugin.php:
--------------------------------------------------------------------------------
1 | register_routes();
44 | }
45 |
46 |
47 | add_filter( 'register_post_type_args', 'wp_api_nav_menus_post_type_args', 10, 2 );
48 |
49 | /**
50 | * Hook in to the nav menu item post type and enable a post type rest endpoint.
51 | *
52 | * @param array $args Current registered post type args.
53 | * @param string $post_type Name of post type.
54 | *
55 | * @return array
56 | */
57 | function wp_api_nav_menus_post_type_args( $args, $post_type ) {
58 | if ( 'nav_menu_item' === $post_type ) {
59 | $args['show_in_rest'] = true;
60 | $args['rest_base'] = 'menu-items';
61 | $args['rest_controller_class'] = 'WP_REST_Menu_Items_Controller';
62 | }
63 |
64 | return $args;
65 | }
66 |
67 |
68 | add_filter( 'register_taxonomy_args', 'wp_api_nav_menus_taxonomy_args', 10, 2 );
69 |
70 | /**
71 | * Hook in to the nav_menu taxonomy and enable a taxonomy rest endpoint.
72 | *
73 | * @param array $args Current registered taxonomy args.
74 | * @param string $taxonomy Name of taxonomy.
75 | *
76 | * @return array
77 | */
78 | function wp_api_nav_menus_taxonomy_args( $args, $taxonomy ) {
79 | if ( 'nav_menu' === $taxonomy ) {
80 | $args['show_in_rest'] = true;
81 | $args['rest_base'] = 'menus';
82 | $args['rest_controller_class'] = 'WP_REST_Menus_Controller';
83 | }
84 |
85 | return $args;
86 | }
87 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | user->create(
50 | array(
51 | 'role' => 'administrator',
52 | )
53 | );
54 | self::$subscriber_id = $factory->user->create(
55 | array(
56 | 'role' => 'subscriber',
57 | )
58 | );
59 | }
60 |
61 | /**
62 | *
63 | */
64 | public static function wpTearDownAfterClass() {
65 | self::delete_user( self::$admin_id );
66 | self::delete_user( self::$subscriber_id );
67 | }
68 |
69 | /**
70 | *
71 | */
72 | public function setUp() {
73 | parent::setUp();
74 |
75 | $this->tag_id = self::factory()->tag->create();
76 |
77 | $this->menu_id = wp_create_nav_menu( rand_str() );
78 |
79 | $this->menu_item_id = wp_update_nav_menu_item(
80 | $this->menu_id,
81 | 0,
82 | array(
83 | 'menu-item-type' => 'taxonomy',
84 | 'menu-item-object' => 'post_tag',
85 | 'menu-item-object-id' => $this->tag_id,
86 | 'menu-item-status' => 'publish',
87 | )
88 | );
89 | }
90 |
91 | /**
92 | *
93 | */
94 | public function test_register_routes() {
95 | $routes = rest_get_server()->get_routes();
96 |
97 | $this->assertArrayHasKey( '/wp/v2/menu-items', $routes );
98 | $this->assertCount( 2, $routes['/wp/v2/menu-items'] );
99 | $this->assertArrayHasKey( '/wp/v2/menu-items/(?P[\d]+)', $routes );
100 | $this->assertCount( 3, $routes['/wp/v2/menu-items/(?P[\d]+)'] );
101 | }
102 |
103 | /**
104 | *
105 | */
106 | public function test_context_param() {
107 | // Collection.
108 | $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menu-items' );
109 | $response = rest_get_server()->dispatch( $request );
110 | $data = $response->get_data();
111 | $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
112 | $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
113 | // Single.
114 | $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menu-items/' . $this->menu_item_id );
115 | $response = rest_get_server()->dispatch( $request );
116 | $data = $response->get_data();
117 | $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
118 | $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
119 | }
120 |
121 | /**
122 | *
123 | */
124 | public function test_registered_query_params() {
125 | $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menu-items' );
126 | $response = rest_get_server()->dispatch( $request );
127 | $data = $response->get_data();
128 | $properties = $data['endpoints'][0]['args'];
129 | $this->assertArrayHasKey( 'before', $properties );
130 | $this->assertArrayHasKey( 'context', $properties );
131 | $this->assertArrayHasKey( 'exclude', $properties );
132 | $this->assertArrayHasKey( 'include', $properties );
133 | $this->assertArrayHasKey( 'menu_order', $properties );
134 | $this->assertArrayHasKey( 'menus', $properties );
135 | $this->assertArrayHasKey( 'menus_exclude', $properties );
136 | $this->assertArrayHasKey( 'offset', $properties );
137 | $this->assertArrayHasKey( 'order', $properties );
138 | $this->assertArrayHasKey( 'orderby', $properties );
139 | $this->assertArrayHasKey( 'page', $properties );
140 | $this->assertArrayHasKey( 'per_page', $properties );
141 | $this->assertArrayHasKey( 'search', $properties );
142 | $this->assertArrayHasKey( 'slug', $properties );
143 | $this->assertArrayHasKey( 'status', $properties );
144 | }
145 |
146 | /**
147 | *
148 | */
149 | public function test_registered_get_item_params() {
150 | $request = new WP_REST_Request( 'OPTIONS', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) );
151 | $response = rest_get_server()->dispatch( $request );
152 | $data = $response->get_data();
153 | $keys = array_keys( $data['endpoints'][0]['args'] );
154 | sort( $keys );
155 | $this->assertEquals( array( 'context', 'id' ), $keys );
156 | }
157 |
158 | /**
159 | *
160 | */
161 | public function test_get_items() {
162 | wp_set_current_user( self::$admin_id );
163 | $request = new WP_REST_Request( 'GET', '/wp/v2/menu-items' );
164 | $response = rest_get_server()->dispatch( $request );
165 |
166 | $this->check_get_menu_items_response( $response );
167 | }
168 |
169 | /**
170 | *
171 | */
172 | public function test_get_item() {
173 | wp_set_current_user( self::$admin_id );
174 | $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) );
175 | $response = rest_get_server()->dispatch( $request );
176 |
177 | $this->check_get_menu_item_response( $response, 'view' );
178 | }
179 |
180 | /**
181 | *
182 | */
183 | public function test_create_item() {
184 | wp_set_current_user( self::$admin_id );
185 |
186 | $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' );
187 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
188 | $params = $this->set_menu_item_data();
189 | $request->set_body_params( $params );
190 | $response = rest_get_server()->dispatch( $request );
191 |
192 | $this->check_create_menu_item_response( $response );
193 | }
194 |
195 | /**
196 | *
197 | */
198 | public function test_create_item_invalid_term() {
199 | wp_set_current_user( self::$admin_id );
200 |
201 | $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' );
202 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
203 | $params = $this->set_menu_item_data(
204 | array(
205 | 'type' => 'taxonomy',
206 | 'title' => 'Tags',
207 | )
208 | );
209 | $request->set_body_params( $params );
210 | $response = rest_get_server()->dispatch( $request );
211 | $this->assertErrorResponse( 'rest_term_invalid_id', $response, 400 );
212 | }
213 |
214 | /**
215 | *
216 | */
217 | public function test_create_item_change_position() {
218 | wp_set_current_user( self::$admin_id );
219 | $new_menu_id = wp_create_nav_menu( rand_str() );
220 | for ( $i = 1; $i < 5; $i ++ ) {
221 | $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' );
222 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
223 | $params = $this->set_menu_item_data(
224 | array(
225 | 'menus' => $new_menu_id,
226 | )
227 | );
228 | $request->set_body_params( $params );
229 | $response = rest_get_server()->dispatch( $request );
230 | $this->check_create_menu_item_response( $response );
231 | $data = $response->get_data();
232 | $this->assertEquals( $data['menu_order'], $i );
233 | }
234 | }
235 |
236 | /**
237 | *
238 | */
239 | public function test_create_item_invalid_position() {
240 | wp_set_current_user( self::$admin_id );
241 | $new_menu_id = wp_create_nav_menu( rand_str() );
242 | $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' );
243 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
244 | $params = $this->set_menu_item_data(
245 | array(
246 | 'menu_order' => 1,
247 | 'menus' => $new_menu_id,
248 | )
249 | );
250 | $request->set_body_params( $params );
251 | $response = rest_get_server()->dispatch( $request );
252 | $this->check_create_menu_item_response( $response );
253 | $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' );
254 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
255 | $params = $this->set_menu_item_data(
256 | array(
257 | 'menu_order' => 1,
258 | 'menus' => $new_menu_id,
259 | )
260 | );
261 | $request->set_body_params( $params );
262 | $response = rest_get_server()->dispatch( $request );
263 |
264 | $this->assertErrorResponse( 'invalid_menu_order', $response, 400 );
265 | }
266 |
267 | /**
268 | *
269 | */
270 | public function test_create_item_invalid_position_2() {
271 | wp_set_current_user( self::$admin_id );
272 | $new_menu_id = wp_create_nav_menu( rand_str() );
273 | $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' );
274 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
275 | $params = $this->set_menu_item_data(
276 | array(
277 | 'menu_order' => 'ddddd',
278 | 'menus' => $new_menu_id,
279 | )
280 | );
281 | $request->set_body_params( $params );
282 | $response = rest_get_server()->dispatch( $request );
283 | $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
284 | }
285 |
286 | /**
287 | *
288 | */
289 | public function test_create_item_invalid_position_3() {
290 | wp_set_current_user( self::$admin_id );
291 | $new_menu_id = wp_create_nav_menu( rand_str() );
292 | $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' );
293 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
294 | $params = $this->set_menu_item_data(
295 | array(
296 | 'menu_order' => -9,
297 | 'menus' => $new_menu_id,
298 | )
299 | );
300 | $request->set_body_params( $params );
301 | $response = rest_get_server()->dispatch( $request );
302 | $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
303 | }
304 |
305 | /**
306 | *
307 | */
308 | public function test_create_item_invalid_parent() {
309 | wp_set_current_user( self::$admin_id );
310 | $new_menu_id = wp_create_nav_menu( rand_str() );
311 | $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' );
312 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
313 | $params = $this->set_menu_item_data(
314 | array(
315 | 'parent' => -9,
316 | )
317 | );
318 | $request->set_body_params( $params );
319 | $response = rest_get_server()->dispatch( $request );
320 | $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
321 | }
322 |
323 | /**
324 | *
325 | */
326 | public function test_create_item_invalid_parent_menu_item() {
327 | wp_set_current_user( self::$admin_id );
328 | $new_menu_id = wp_create_nav_menu( rand_str() );
329 | $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' );
330 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
331 | $params = $this->set_menu_item_data(
332 | array(
333 | 'menus' => $new_menu_id,
334 | 'parent' => $this->menu_item_id,
335 | )
336 | );
337 | $request->set_body_params( $params );
338 | $response = rest_get_server()->dispatch( $request );
339 | $this->assertErrorResponse( 'invalid_item_parent', $response, 400 );
340 | }
341 |
342 | /**
343 | *
344 | */
345 | public function test_create_item_invalid_parent_post() {
346 | wp_set_current_user( self::$admin_id );
347 | $post_id = self::factory()->post->create();
348 | $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' );
349 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
350 | $params = $this->set_menu_item_data(
351 | array(
352 | 'parent' => $post_id,
353 | )
354 | );
355 | $request->set_body_params( $params );
356 | $response = rest_get_server()->dispatch( $request );
357 | $this->assertErrorResponse( 'invalid_menu_item_parent', $response, 400 );
358 | }
359 |
360 | /**
361 | *
362 | */
363 | public function test_create_item_invalid_menu() {
364 | wp_set_current_user( self::$admin_id );
365 | $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' );
366 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
367 | $params = $this->set_menu_item_data(
368 | array(
369 | 'menus' => -9,
370 | )
371 | );
372 | $request->set_body_params( $params );
373 | $response = rest_get_server()->dispatch( $request );
374 | $this->assertErrorResponse( 'invalid_menu_id', $response, 400 );
375 | }
376 |
377 | /**
378 | *
379 | */
380 | public function test_create_item_invalid_post() {
381 | wp_set_current_user( self::$admin_id );
382 |
383 | $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' );
384 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
385 | $params = $this->set_menu_item_data(
386 | array(
387 | 'type' => 'post_type',
388 | 'title' => 'Post',
389 | )
390 | );
391 | $request->set_body_params( $params );
392 | $response = rest_get_server()->dispatch( $request );
393 | $this->assertErrorResponse( 'rest_post_invalid_id', $response, 400 );
394 | }
395 |
396 | /**
397 | *
398 | */
399 | public function test_create_item_invalid_post_type() {
400 | wp_set_current_user( self::$admin_id );
401 |
402 | $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' );
403 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
404 | $params = $this->set_menu_item_data(
405 | array(
406 | 'type' => 'post_type_archive',
407 | 'menu-item-object' => 'invalid_post_type',
408 | )
409 | );
410 | $request->set_body_params( $params );
411 | $response = rest_get_server()->dispatch( $request );
412 | $this->assertErrorResponse( 'rest_post_invalid_type', $response, 400 );
413 | }
414 |
415 | /**
416 | *
417 | */
418 | public function test_create_item_invalid_custom_link() {
419 | wp_set_current_user( self::$admin_id );
420 |
421 | $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' );
422 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
423 | $params = $this->set_menu_item_data(
424 | array(
425 | 'type' => 'custom',
426 | 'title' => '',
427 | )
428 | );
429 | $request->set_body_params( $params );
430 | $response = rest_get_server()->dispatch( $request );
431 | $this->assertErrorResponse( 'rest_title_required', $response, 400 );
432 | }
433 |
434 | /**
435 | *
436 | */
437 | public function test_create_item_invalid_custom_link_url() {
438 | wp_set_current_user( self::$admin_id );
439 |
440 | $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' );
441 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
442 | $params = $this->set_menu_item_data(
443 | array(
444 | 'type' => 'custom',
445 | 'url' => '',
446 | )
447 | );
448 | $request->set_body_params( $params );
449 | $response = rest_get_server()->dispatch( $request );
450 | $this->assertErrorResponse( 'rest_url_required', $response, 400 );
451 | }
452 |
453 | /**
454 | *
455 | */
456 | public function test_update_item() {
457 | wp_set_current_user( self::$admin_id );
458 |
459 | $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) );
460 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
461 | $params = $this->set_menu_item_data(
462 | array(
463 | 'xfn' => array( 'test1', 'test2', 'test3' ),
464 | )
465 | );
466 | $request->set_body_params( $params );
467 | $response = rest_get_server()->dispatch( $request );
468 | $this->check_update_menu_item_response( $response );
469 | $new_data = $response->get_data();
470 | $this->assertEquals( $this->menu_item_id, $new_data['id'] );
471 | $this->assertEquals( $params['title'], $new_data['title']['raw'] );
472 | $this->assertEquals( $params['description'], $new_data['description'] );
473 | $this->assertEquals( $params['type_label'], $new_data['type_label'] );
474 | $this->assertEquals( $params['xfn'], $new_data['xfn'] );
475 | $post = get_post( $this->menu_item_id );
476 | $menu_item = wp_setup_nav_menu_item( $post );
477 | $this->assertEquals( $params['title'], $menu_item->title );
478 | $this->assertEquals( $params['description'], $menu_item->description );
479 | $this->assertEquals( $params['xfn'], explode( ' ', $menu_item->xfn ) );
480 | }
481 |
482 | /**
483 | *
484 | */
485 | public function test_update_item_clean_xfn() {
486 | wp_set_current_user( self::$admin_id );
487 |
488 | $bad_data = array( 'test1":|":', 'test2+|+', 'test3±', 'test4😀' );
489 | $good_data = array( 'test1', 'test2', 'test3', 'test4' );
490 |
491 | $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) );
492 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
493 | $params = $this->set_menu_item_data(
494 | array(
495 | 'xfn' => $bad_data,
496 | )
497 | );
498 | $request->set_body_params( $params );
499 | $response = rest_get_server()->dispatch( $request );
500 | $this->check_update_menu_item_response( $response );
501 | $new_data = $response->get_data();
502 | $this->assertEquals( $this->menu_item_id, $new_data['id'] );
503 | $this->assertEquals( $params['title'], $new_data['title']['raw'] );
504 | $this->assertEquals( $params['description'], $new_data['description'] );
505 | $this->assertEquals( $params['type_label'], $new_data['type_label'] );
506 | $this->assertEquals( $good_data, $new_data['xfn'] );
507 | $post = get_post( $this->menu_item_id );
508 | $menu_item = wp_setup_nav_menu_item( $post );
509 | $this->assertEquals( $params['title'], $menu_item->title );
510 | $this->assertEquals( $params['description'], $menu_item->description );
511 | $this->assertEquals( $good_data, explode( ' ', $menu_item->xfn ) );
512 | }
513 |
514 |
515 | /**
516 | *
517 | */
518 | public function test_update_item_invalid() {
519 | wp_set_current_user( self::$admin_id );
520 | $post_id = self::factory()->post->create();
521 |
522 | $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/menu-items/%d', $post_id ) );
523 | $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
524 | $params = $this->set_menu_item_data();
525 | $request->set_body_params( $params );
526 | $response = rest_get_server()->dispatch( $request );
527 | $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
528 | }
529 |
530 | /**
531 | *
532 | */
533 | public function test_delete_item() {
534 | wp_set_current_user( self::$admin_id );
535 | $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) );
536 | $request->set_param( 'force', true );
537 | $response = rest_get_server()->dispatch( $request );
538 | $this->assertEquals( 200, $response->get_status() );
539 | $this->assertNull( get_post( $this->menu_item_id ) );
540 | }
541 |
542 |
543 | /**
544 | *
545 | */
546 | public function test_delete_item_no_force() {
547 | wp_set_current_user( self::$admin_id );
548 | $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) );
549 | $request->set_param( 'force', false );
550 | $response = rest_get_server()->dispatch( $request );
551 | $this->assertEquals( 501, $response->get_status() );
552 | $this->assertNotNull( get_post( $this->menu_item_id ) );
553 | }
554 |
555 | /**
556 | *
557 | */
558 | public function test_prepare_item() {
559 | wp_set_current_user( self::$admin_id );
560 | $request = new WP_REST_Request( 'GET', '/wp/v2/menu-items/' . $this->menu_item_id );
561 | $response = rest_get_server()->dispatch( $request );
562 | $this->assertEquals( 200, $response->get_status() );
563 | $this->check_get_menu_item_response( $response );
564 | }
565 |
566 | /**
567 | *
568 | */
569 | public function test_get_item_schema() {
570 | $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menu-items' );
571 | $response = rest_get_server()->dispatch( $request );
572 | $data = $response->get_data();
573 | $properties = $data['schema']['properties'];
574 | $this->assertEquals( 18, count( $properties ) );
575 | $this->assertArrayHasKey( 'type_label', $properties );
576 | $this->assertArrayHasKey( 'attr_title', $properties );
577 | $this->assertArrayHasKey( 'classes', $properties );
578 | $this->assertArrayHasKey( 'description', $properties );
579 | $this->assertArrayHasKey( 'id', $properties );
580 | $this->assertArrayHasKey( 'url', $properties );
581 | $this->assertArrayHasKey( 'meta', $properties );
582 | $this->assertArrayHasKey( 'menu_order', $properties );
583 | $this->assertArrayHasKey( 'object', $properties );
584 | $this->assertArrayHasKey( 'object_id', $properties );
585 | $this->assertArrayHasKey( 'target', $properties );
586 | $this->assertArrayHasKey( 'parent', $properties );
587 | $this->assertArrayHasKey( 'status', $properties );
588 | $this->assertArrayHasKey( 'title', $properties );
589 | $this->assertArrayHasKey( 'type', $properties );
590 | $this->assertArrayHasKey( 'xfn', $properties );
591 | $this->assertArrayHasKey( '_invalid', $properties );
592 | }
593 |
594 | /**
595 | *
596 | */
597 | public function test_get_items_no_permission() {
598 | wp_set_current_user( 0 );
599 | $request = new WP_REST_Request( 'GET', '/wp/v2/menu-items' );
600 | $response = rest_get_server()->dispatch( $request );
601 | $this->assertErrorResponse( 'rest_cannot_view', $response, 401 );
602 | }
603 |
604 | /**
605 | *
606 | */
607 | public function test_get_item_no_permission() {
608 | wp_set_current_user( 0 );
609 | $request = new WP_REST_Request( 'GET', '/wp/v2/menu-items/' . $this->menu_item_id );
610 | $response = rest_get_server()->dispatch( $request );
611 | $this->assertErrorResponse( 'rest_cannot_view', $response, 401 );
612 | }
613 |
614 | /**
615 | *
616 | */
617 | public function test_get_items_wrong_permission() {
618 | wp_set_current_user( self::$subscriber_id );
619 | $request = new WP_REST_Request( 'GET', '/wp/v2/menu-items' );
620 | $response = rest_get_server()->dispatch( $request );
621 | $this->assertErrorResponse( 'rest_cannot_view', $response, 403 );
622 | }
623 |
624 | /**
625 | *
626 | */
627 | public function test_get_item_wrong_permission() {
628 | wp_set_current_user( self::$subscriber_id );
629 | $request = new WP_REST_Request( 'GET', '/wp/v2/menu-items/' . $this->menu_item_id );
630 | $response = rest_get_server()->dispatch( $request );
631 | $this->assertErrorResponse( 'rest_cannot_view', $response, 403 );
632 | }
633 |
634 | /**
635 | * @param WP_REST_Response $response Response Class.
636 | * @param string $context Defaults to View.
637 | */
638 | protected function check_get_menu_items_response( $response, $context = 'view' ) {
639 | $this->assertNotWPError( $response );
640 | $response = rest_ensure_response( $response );
641 | $this->assertEquals( 200, $response->get_status() );
642 |
643 | $headers = $response->get_headers();
644 | $this->assertArrayHasKey( 'X-WP-Total', $headers );
645 | $this->assertArrayHasKey( 'X-WP-TotalPages', $headers );
646 |
647 | $all_data = $response->get_data();
648 | foreach ( $all_data as $data ) {
649 | $post = get_post( $data['id'] );
650 | // Base fields for every post.
651 | $menu_item = wp_setup_nav_menu_item( $post );
652 | /**
653 | * as the links for the post are "response_links" format in the data array we have to pull them out and parse them.
654 | */
655 | $links = $data['_links'];
656 | foreach ( $links as &$links_array ) {
657 | foreach ( $links_array as &$link ) {
658 | $attributes = array_diff_key(
659 | $link,
660 | array(
661 | 'href' => 1,
662 | 'name' => 1,
663 | )
664 | );
665 | $link = array_diff_key( $link, $attributes );
666 | $link['attributes'] = $attributes;
667 | }
668 | }
669 |
670 | $this->check_menu_item_data( $menu_item, $data, $context, $links );
671 | }
672 | }
673 |
674 | /**
675 | * @param WP_Post $post WP_Post object.
676 | * @param array $data Data compare.
677 | * @param string $context Context of REST Request.
678 | * @param array $links Array links.
679 | */
680 | protected function check_menu_item_data( $post, $data, $context, $links ) {
681 | $post_type_obj = get_post_type_object( self::POST_TYPE );
682 |
683 | // Standard fields.
684 | $this->assertEquals( $post->ID, $data['id'] );
685 | $this->assertEquals( wpautop( $post->post_content ), $data['description'] );
686 |
687 | // Check filtered values.
688 | if ( post_type_supports( self::POST_TYPE, 'title' ) ) {
689 | add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
690 | $this->assertEquals( $post->title, $data['title']['rendered'] );
691 | remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
692 | if ( 'edit' === $context ) {
693 | $this->assertEquals( $post->post_title, $data['title']['raw'] );
694 | } else {
695 | $this->assertFalse( isset( $data['title']['raw'] ) );
696 | }
697 | } else {
698 | $this->assertFalse( isset( $data['title'] ) );
699 | }
700 |
701 | // post_parent.
702 | $this->assertArrayHasKey( 'parent', $data );
703 | if ( $post->post_parent ) {
704 | if ( is_int( $data['parent'] ) ) {
705 | $this->assertEquals( $post->post_parent, $data['parent'] );
706 | } else {
707 | $this->assertEquals( $post->post_parent, $data['parent']['id'] );
708 | $menu_item = wp_setup_nav_menu_item( get_post( $data['parent']['id'] ) );
709 | $this->check_get_menu_item_response( $data['parent'], $menu_item, 'view-parent' );
710 | }
711 | } else {
712 | $this->assertEmpty( $data['parent'] );
713 | }
714 |
715 | // page attributes.
716 | $this->assertEquals( $post->menu_order, $data['menu_order'] );
717 |
718 | $taxonomies = wp_list_filter( get_object_taxonomies( self::POST_TYPE, 'objects' ), array( 'show_in_rest' => true ) );
719 | foreach ( $taxonomies as $taxonomy ) {
720 | $this->assertTrue( isset( $data[ $taxonomy->rest_base ] ) );
721 | $terms = wp_get_object_terms( $post->ID, $taxonomy->name, array( 'fields' => 'ids' ) );
722 | sort( $terms );
723 | sort( $data[ $taxonomy->rest_base ] );
724 | $this->assertEquals( $terms, $data[ $taxonomy->rest_base ] );
725 | }
726 |
727 | // test links.
728 | if ( $links ) {
729 | $links = test_rest_expand_compact_links( $links );
730 | $this->assertEquals( $links['self'][0]['href'], rest_url( 'wp/v2/' . $post_type_obj->rest_base . '/' . $data['id'] ) );
731 | $this->assertEquals( $links['collection'][0]['href'], rest_url( 'wp/v2/' . $post_type_obj->rest_base ) );
732 | $this->assertEquals( $links['about'][0]['href'], rest_url( 'wp/v2/types/' . self::POST_TYPE ) );
733 |
734 | $num = 0;
735 | foreach ( $taxonomies as $key => $taxonomy ) {
736 | $this->assertEquals( $taxonomy->name, $links['https://api.w.org/term'][ $num ]['attributes']['taxonomy'] );
737 | $this->assertEquals( add_query_arg( 'post', $data['id'], rest_url( 'wp/v2/' . $taxonomy->rest_base ) ), $links['https://api.w.org/term'][ $num ]['href'] );
738 | $num ++;
739 | }
740 | }
741 | }
742 |
743 | /**
744 | * @param WP_REST_Response $response Response Class.
745 | * @param string $context Defaults to View.
746 | */
747 | protected function check_get_menu_item_response( $response, $context = 'view' ) {
748 | $this->assertNotWPError( $response );
749 | $response = rest_ensure_response( $response );
750 | $this->assertEquals( 200, $response->get_status() );
751 |
752 | $data = $response->get_data();
753 | $post = get_post( $data['id'] );
754 | $menu_item = wp_setup_nav_menu_item( $post );
755 | $this->check_menu_item_data( $menu_item, $data, $context, $response->get_links() );
756 | }
757 |
758 | /**
759 | * @param WP_REST_Response $response Response Class.
760 | */
761 | protected function check_create_menu_item_response( $response ) {
762 | $this->assertNotWPError( $response );
763 | $response = rest_ensure_response( $response );
764 |
765 | $this->assertEquals( 201, $response->get_status() );
766 | $headers = $response->get_headers();
767 | $this->assertArrayHasKey( 'Location', $headers );
768 |
769 | $data = $response->get_data();
770 | $post = get_post( $data['id'] );
771 | $menu_item = wp_setup_nav_menu_item( $post );
772 | $this->check_menu_item_data( $menu_item, $data, 'edit', $response->get_links() );
773 | }
774 |
775 | /**
776 | * @param WP_REST_Response $response Response Class.
777 | */
778 | protected function check_update_menu_item_response( $response ) {
779 | $this->assertNotWPError( $response );
780 | $response = rest_ensure_response( $response );
781 |
782 | $this->assertEquals( 200, $response->get_status() );
783 | $headers = $response->get_headers();
784 | $this->assertArrayNotHasKey( 'Location', $headers );
785 |
786 | $data = $response->get_data();
787 | $post = get_post( $data['id'] );
788 | $menu_item = wp_setup_nav_menu_item( $post );
789 | $this->check_menu_item_data( $menu_item, $data, 'edit', $response->get_links() );
790 | }
791 |
792 | /**
793 | * @param array $args Override params.
794 | *
795 | * @return mixed
796 | */
797 | protected function set_menu_item_data( $args = array() ) {
798 | $defaults = array(
799 | 'object_id' => 0,
800 | 'parent' => 0,
801 | 'menu_order' => 0,
802 | 'menus' => $this->menu_id,
803 | 'type' => 'custom',
804 | 'title' => 'Custom Link Title',
805 | 'url' => '#',
806 | 'description' => '',
807 | 'attr-title' => '',
808 | 'target' => '',
809 | 'type_label' => 'Custom Link',
810 | 'classes' => '',
811 | 'xfn' => '',
812 | 'status' => 'draft',
813 | );
814 |
815 | return wp_parse_args( $args, $defaults );
816 | }
817 | }
818 |
--------------------------------------------------------------------------------
/tests/test-rest-nav-menu-locations.php:
--------------------------------------------------------------------------------
1 | user->create(
28 | array(
29 | 'role' => 'administrator',
30 | )
31 | );
32 | }
33 |
34 | /**
35 | * Set up.
36 | */
37 | public function setUp() {
38 | parent::setUp();
39 |
40 | // Unregister all nav menu locations.
41 | foreach ( array_keys( get_registered_nav_menus() ) as $location ) {
42 | unregister_nav_menu( $location );
43 | }
44 | }
45 |
46 | /**
47 | * Register nav menu locations.
48 | *
49 | * @param array $locations Location slugs.
50 | */
51 | public function register_nav_menu_locations( $locations ) {
52 | foreach ( $locations as $location ) {
53 | register_nav_menu( $location, ucfirst( $location ) );
54 | }
55 | }
56 |
57 | /**
58 | *
59 | */
60 | public function test_register_routes() {
61 | $routes = rest_get_server()->get_routes();
62 | $this->assertArrayHasKey( '/wp/v2/menu-locations', $routes );
63 | $this->assertCount( 1, $routes['/wp/v2/menu-locations'] );
64 | $this->assertArrayHasKey( '/wp/v2/menu-locations/(?P[\w-]+)', $routes );
65 | $this->assertCount( 1, $routes['/wp/v2/menu-locations/(?P[\w-]+)'] );
66 | }
67 |
68 | /**
69 | *
70 | */
71 | public function test_context_param() {
72 | // Collection.
73 | $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menu-locations' );
74 | $response = rest_get_server()->dispatch( $request );
75 | $data = $response->get_data();
76 | $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
77 | $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
78 | $menu = 'primary';
79 | $this->register_nav_menu_locations( array( $menu ) );
80 | $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menu-locations/' . $menu );
81 | $response = rest_get_server()->dispatch( $request );
82 | $data = $response->get_data();
83 | $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
84 | $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
85 | }
86 |
87 | /**
88 | *
89 | */
90 | public function test_get_items() {
91 | $menus = array( 'primary', 'secondary' );
92 | $this->register_nav_menu_locations( array( 'primary', 'secondary' ) );
93 | wp_set_current_user( self::$admin_id );
94 | $request = new WP_REST_Request( 'GET', '/wp/v2/menu-locations' );
95 | $response = rest_get_server()->dispatch( $request );
96 | $data = $response->get_data();
97 | $data = array_values( $data );
98 | $this->assertCount( 2, $data );
99 | $names = wp_list_pluck( $data, 'name' );
100 | $descriptions = wp_list_pluck( $data, 'description' );
101 | $this->assertEquals( $menus, $names );
102 | $menu_descriptions = array_map( 'ucfirst', $names );
103 | $this->assertEquals( $menu_descriptions, $descriptions );
104 | }
105 |
106 | /**
107 | *
108 | */
109 | public function test_get_item() {
110 | $menu = 'primary';
111 | $this->register_nav_menu_locations( array( $menu ) );
112 |
113 | wp_set_current_user( self::$admin_id );
114 | $request = new WP_REST_Request( 'GET', '/wp/v2/menu-locations/' . $menu );
115 | $response = rest_get_server()->dispatch( $request );
116 | $data = $response->get_data();
117 | $this->assertEquals( $menu, $data['name'] );
118 | }
119 |
120 | /**
121 | * The test_create_item() method does not exist for menu locations.
122 | */
123 | public function test_create_item() {}
124 |
125 | /**
126 | * The test_update_item() method does not exist for menu locations.
127 | */
128 | public function test_update_item() {}
129 |
130 | /**
131 | * The test_delete_item() method does not exist for menu locations.
132 | */
133 | public function test_delete_item() {}
134 |
135 | /**
136 | * The test_prepare_item() method does not exist for menu locations.
137 | */
138 | public function test_prepare_item() {}
139 |
140 | /**
141 | *
142 | */
143 | public function test_get_item_schema() {
144 | wp_set_current_user( self::$admin_id );
145 | $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menu-locations' );
146 | $response = rest_get_server()->dispatch( $request );
147 | $data = $response->get_data();
148 | $properties = $data['schema']['properties'];
149 | $this->assertEquals( 3, count( $properties ) );
150 | $this->assertArrayHasKey( 'name', $properties );
151 | $this->assertArrayHasKey( 'description', $properties );
152 | $this->assertArrayHasKey( 'menu', $properties );
153 | }
154 |
155 |
156 | /**
157 | *
158 | */
159 | public function test_get_item_menu_location_context_without_permission() {
160 | wp_set_current_user( 0 );
161 | $request = new WP_REST_Request( 'GET', '/wp/v2/menu-locations' );
162 | $response = rest_get_server()->dispatch( $request );
163 |
164 | $this->assertErrorResponse( 'rest_cannot_view', $response, rest_authorization_required_code() );
165 | }
166 |
167 | /**
168 | *
169 | */
170 | public function test_get_items_menu_location_context_without_permission() {
171 | $menu = 'primary';
172 | $this->register_nav_menu_locations( array( $menu ) );
173 |
174 | wp_set_current_user( 0 );
175 | $request = new WP_REST_Request( 'GET', '/wp/v2/menu-locations/' . $menu );
176 | $response = rest_get_server()->dispatch( $request );
177 |
178 | $this->assertErrorResponse( 'rest_cannot_view', $response, rest_authorization_required_code() );
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/tests/test-rest-nav-menus-controller.php:
--------------------------------------------------------------------------------
1 | user->create(
47 | array(
48 | 'role' => 'administrator',
49 | )
50 | );
51 | self::$subscriber_id = $factory->user->create(
52 | array(
53 | 'role' => 'subscriber',
54 | )
55 | );
56 | }
57 |
58 | /**
59 | *
60 | */
61 | public function setUp() {
62 | parent::setUp();
63 | // Unregister all nav menu locations.
64 | foreach ( array_keys( get_registered_nav_menus() ) as $location ) {
65 | unregister_nav_menu( $location );
66 | }
67 |
68 | $orig_args = array(
69 | 'name' => 'Original Name',
70 | 'description' => 'Original Description',
71 | 'slug' => 'original-slug',
72 | 'taxonomy' => 'nav_menu',
73 | );
74 |
75 | $this->menu_id = $this->factory->term->create( $orig_args );
76 |
77 | register_meta(
78 | 'term',
79 | 'test_single_menu',
80 | array(
81 | 'object_subtype' => self::TAXONOMY,
82 | 'show_in_rest' => true,
83 | 'single' => true,
84 | 'type' => 'string',
85 | )
86 | );
87 | }
88 |
89 | /**
90 | * Register nav menu locations.
91 | *
92 | * @param array $locations Location slugs.
93 | */
94 | public function register_nav_menu_locations( $locations ) {
95 | foreach ( $locations as $location ) {
96 | register_nav_menu( $location, ucfirst( $location ) );
97 | }
98 | }
99 |
100 | /**
101 | *
102 | */
103 | public function test_register_routes() {
104 | $routes = rest_get_server()->get_routes();
105 | $this->assertArrayHasKey( '/wp/v2/menus', $routes );
106 | $this->assertArrayHasKey( '/wp/v2/menus/(?P[\d]+)', $routes );
107 | }
108 |
109 | /**
110 | *
111 | */
112 | public function test_context_param() {
113 | // Collection.
114 | $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menus' );
115 | $response = rest_get_server()->dispatch( $request );
116 | $data = $response->get_data();
117 | $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
118 | $this->assertEqualSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
119 | // Single.
120 | $tag1 = $this->factory->tag->create( array( 'name' => 'Season 5' ) );
121 | $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menus/' . $tag1 );
122 | $response = rest_get_server()->dispatch( $request );
123 | $data = $response->get_data();
124 | $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
125 | $this->assertEqualSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
126 | }
127 |
128 | /**
129 | *
130 | */
131 | public function test_registered_query_params() {
132 | $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menus' );
133 | $response = rest_get_server()->dispatch( $request );
134 | $data = $response->get_data();
135 | $keys = array_keys( $data['endpoints'][0]['args'] );
136 | sort( $keys );
137 | $this->assertEquals(
138 | array(
139 | 'context',
140 | 'exclude',
141 | 'hide_empty',
142 | 'include',
143 | 'offset',
144 | 'order',
145 | 'orderby',
146 | 'page',
147 | 'per_page',
148 | 'post',
149 | 'search',
150 | 'slug',
151 | ),
152 | $keys
153 | );
154 | }
155 |
156 | /**
157 | *
158 | */
159 | public function test_get_items() {
160 | wp_set_current_user( self::$admin_id );
161 | $nav_menu_id = wp_update_nav_menu_object(
162 | 0,
163 | array(
164 | 'description' => 'Test get',
165 | 'menu-name' => 'test Name get',
166 | )
167 | );
168 | $request = new WP_REST_Request( 'GET', '/wp/v2/menus' );
169 | $request->set_param( 'per_page', self::$per_page );
170 | $response = rest_get_server()->dispatch( $request );
171 | $this->check_get_taxonomy_terms_response( $response );
172 | }
173 |
174 | /**
175 | *
176 | */
177 | public function test_get_item() {
178 | wp_set_current_user( self::$admin_id );
179 | $nav_menu_id = wp_update_nav_menu_object(
180 | 0,
181 | array(
182 | 'description' => 'Test menu',
183 | 'menu-name' => 'test Name',
184 | )
185 | );
186 | $request = new WP_REST_Request( 'GET', '/wp/v2/menus/' . $nav_menu_id );
187 | $response = rest_get_server()->dispatch( $request );
188 | $this->check_get_taxonomy_term_response( $response, $nav_menu_id );
189 | }
190 |
191 | /**
192 | *
193 | */
194 | public function test_create_item() {
195 | wp_set_current_user( self::$admin_id );
196 |
197 | $request = new WP_REST_Request( 'POST', '/wp/v2/menus' );
198 | $request->set_param( 'name', 'My Awesome menus' );
199 | $request->set_param( 'description', 'This menu is so awesome.' );
200 | $response = rest_get_server()->dispatch( $request );
201 | $this->assertEquals( 201, $response->get_status() );
202 | $headers = $response->get_headers();
203 | $data = $response->get_data();
204 | $this->assertContains( '/wp/v2/menus/' . $data['id'], $headers['Location'] );
205 | $this->assertEquals( 'My Awesome menus', $data['name'] );
206 | $this->assertEquals( 'This menu is so awesome.', $data['description'] );
207 | $this->assertEquals( 'my-awesome-menus', $data['slug'] );
208 | }
209 |
210 | /**
211 | *
212 | */
213 | public function test_update_item() {
214 | wp_set_current_user( self::$admin_id );
215 |
216 | $request = new WP_REST_Request( 'POST', '/wp/v2/menus/' . $this->menu_id );
217 | $request->set_param( 'name', 'New Name' );
218 | $request->set_param( 'description', 'New Description' );
219 | $request->set_param(
220 | 'meta',
221 | array(
222 | 'test_single_menu' => 'just meta',
223 | )
224 | );
225 | $response = rest_get_server()->dispatch( $request );
226 | $this->assertEquals( 200, $response->get_status() );
227 | $data = $response->get_data();
228 | $this->assertEquals( 'New Name', $data['name'] );
229 | $this->assertEquals( 'New Description', $data['description'] );
230 | $this->assertEquals( 'new-name', $data['slug'] );
231 | $this->assertEquals( 'just meta', $data['meta']['test_single_menu'] );
232 | $this->assertFalse( isset( $data['meta']['test_cat_meta'] ) );
233 | }
234 |
235 | /**
236 | *
237 | */
238 | public function test_delete_item() {
239 | wp_set_current_user( self::$admin_id );
240 |
241 | $nav_menu_id = wp_update_nav_menu_object(
242 | 0,
243 | array(
244 | 'description' => 'Deleted Menu',
245 | 'menu-name' => 'Deleted Menu',
246 | )
247 | );
248 |
249 | $term = get_term_by( 'id', $nav_menu_id, self::TAXONOMY );
250 |
251 | $request = new WP_REST_Request( 'DELETE', '/wp/v2/menus/' . $term->term_id );
252 | $request->set_param( 'force', true );
253 | $response = rest_get_server()->dispatch( $request );
254 | $this->assertEquals( 200, $response->get_status() );
255 | $data = $response->get_data();
256 | $this->assertTrue( $data['deleted'] );
257 | $this->assertEquals( 'Deleted Menu', $data['previous']['name'] );
258 | }
259 |
260 | /**
261 | *
262 | */
263 | public function test_prepare_item() {
264 | $nav_menu_id = wp_update_nav_menu_object(
265 | 0,
266 | array(
267 | 'description' => 'Foo Menu',
268 | 'menu-name' => 'Foo Menu',
269 | )
270 | );
271 |
272 | $term = get_term_by( 'id', $nav_menu_id, self::TAXONOMY );
273 | wp_set_current_user( self::$admin_id );
274 | $request = new WP_REST_Request( 'GET', '/wp/v2/menus/' . $term->term_id );
275 | $response = rest_get_server()->dispatch( $request );
276 | $data = $response->get_data();
277 |
278 | $this->check_taxonomy_term( $term, $data, $response->get_links() );
279 | }
280 |
281 | /**
282 | *
283 | */
284 | public function test_get_item_schema() {
285 | $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menus' );
286 | $response = rest_get_server()->dispatch( $request );
287 | $data = $response->get_data();
288 | $properties = $data['schema']['properties'];
289 | $this->assertEquals( 6, count( $properties ) );
290 | $this->assertArrayHasKey( 'id', $properties );
291 | $this->assertArrayHasKey( 'description', $properties );
292 | $this->assertArrayHasKey( 'meta', $properties );
293 | $this->assertArrayHasKey( 'name', $properties );
294 | $this->assertArrayHasKey( 'slug', $properties );
295 | $this->assertArrayHasKey( 'locations', $properties );
296 | }
297 |
298 | /**
299 | *
300 | */
301 | public function test_create_item_with_location_permission_correct() {
302 | $this->register_nav_menu_locations( array( 'primary', 'secondary' ) );
303 | wp_set_current_user( self::$admin_id );
304 | $request = new WP_REST_Request( 'POST', '/wp/v2/menus' );
305 | $request->set_param( 'name', 'My Awesome Term' );
306 | $request->set_param( 'slug', 'so-awesome' );
307 | $request->set_param( 'locations', 'primary' );
308 | $response = rest_get_server()->dispatch( $request );
309 | $this->assertEquals( 201, $response->get_status() );
310 | $data = $response->get_data();
311 | $term_id = $data['id'];
312 | $locations = get_nav_menu_locations();
313 | $this->assertEquals( $locations['primary'], $term_id );
314 | }
315 |
316 | /**
317 | *
318 | */
319 | public function test_create_item_with_location_permission_incorrect() {
320 | wp_set_current_user( self::$subscriber_id );
321 | $request = new WP_REST_Request( 'POST', '/wp/v2/menus' );
322 | $request->set_param( 'name', 'My Awesome Term' );
323 | $request->set_param( 'slug', 'so-awesome' );
324 | $request->set_param( 'locations', 'primary' );
325 | $response = rest_get_server()->dispatch( $request );
326 | $this->assertEquals( rest_authorization_required_code(), $response->get_status() );
327 | $this->assertErrorResponse( 'rest_cannot_assign_location', $response, rest_authorization_required_code() );
328 | }
329 |
330 | /**
331 | *
332 | */
333 | public function test_create_item_with_location_permission_no_location() {
334 | wp_set_current_user( self::$admin_id );
335 | $request = new WP_REST_Request( 'POST', '/wp/v2/menus' );
336 | $request->set_param( 'name', 'My Awesome Term' );
337 | $request->set_param( 'slug', 'so-awesome' );
338 | $request->set_param( 'locations', 'bar' );
339 | $response = rest_get_server()->dispatch( $request );
340 | $this->assertEquals( 400, $response->get_status() );
341 | $this->assertErrorResponse( 'rest_menu_location_invalid', $response, 400 );
342 | }
343 |
344 | /**
345 | *
346 | */
347 | public function test_update_item_with_no_location() {
348 | $this->register_nav_menu_locations( array( 'primary', 'secondary' ) );
349 | wp_set_current_user( self::$admin_id );
350 |
351 | $request = new WP_REST_Request( 'POST', '/wp/v2/menus/' . $this->menu_id );
352 | $request->set_param( 'name', 'New Name' );
353 | $request->set_param( 'description', 'New Description' );
354 | $request->set_param( 'slug', 'new-slug' );
355 | $request->set_param( 'locations', 'bar' );
356 | $response = rest_get_server()->dispatch( $request );
357 | $this->assertEquals( 400, $response->get_status() );
358 | }
359 |
360 | /**
361 | *
362 | */
363 | public function test_update_item_with_location_permission_correct() {
364 | $this->register_nav_menu_locations( array( 'primary', 'secondary' ) );
365 | wp_set_current_user( self::$admin_id );
366 | $request = new WP_REST_Request( 'POST', '/wp/v2/menus/' . $this->menu_id );
367 | $request->set_param( 'name', 'New Name' );
368 | $request->set_param( 'description', 'New Description' );
369 | $request->set_param( 'slug', 'new-slug' );
370 | $request->set_param( 'locations', 'primary' );
371 | $response = rest_get_server()->dispatch( $request );
372 | $this->assertEquals( 200, $response->get_status() );
373 | $locations = get_nav_menu_locations();
374 | $this->assertEquals( $locations['primary'], $this->menu_id );
375 | }
376 |
377 | /**
378 | *
379 | */
380 | public function test_update_item_with_location_permission_incorrect() {
381 | $this->register_nav_menu_locations( array( 'primary', 'secondary' ) );
382 | wp_set_current_user( self::$subscriber_id );
383 | $request = new WP_REST_Request( 'POST', '/wp/v2/menus/' . $this->menu_id );
384 | $request->set_param( 'name', 'New Name' );
385 | $request->set_param( 'description', 'New Description' );
386 | $request->set_param( 'slug', 'new-slug' );
387 | $request->set_param( 'locations', 'primary' );
388 | $response = rest_get_server()->dispatch( $request );
389 | $this->assertEquals( rest_authorization_required_code(), $response->get_status() );
390 | }
391 |
392 | /**
393 | *
394 | */
395 | public function test_get_item_links() {
396 | wp_set_current_user( self::$admin_id );
397 |
398 | $nav_menu_id = wp_update_nav_menu_object(
399 | 0,
400 | array(
401 | 'description' => 'Foo Menu',
402 | 'menu-name' => 'Foo Menu',
403 | )
404 | );
405 |
406 | register_nav_menu( 'foo', 'Bar' );
407 |
408 | set_theme_mod( 'nav_menu_locations', array( 'foo' => $nav_menu_id ) );
409 |
410 | $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/menus/%d', $nav_menu_id ) );
411 | $response = rest_get_server()->dispatch( $request );
412 |
413 | $links = $response->get_links();
414 | $this->assertArrayHasKey( 'https://api.w.org/menu-location', $links );
415 |
416 | $location_url = rest_url( '/wp/v2/menu-locations/foo' );
417 | $this->assertEquals( $location_url, $links['https://api.w.org/menu-location'][0]['href'] );
418 | }
419 |
420 | /**
421 | *
422 | */
423 | public function test_change_menu_location() {
424 | $this->register_nav_menu_locations( array( 'primary', 'secondary' ) );
425 | $secondary_id = self::factory()->term->create(
426 | array(
427 | 'name' => 'Secondary Name',
428 | 'description' => 'Secondary Description',
429 | 'slug' => 'secondary-slug',
430 | 'taxonomy' => 'nav_menu',
431 | )
432 | );
433 |
434 | $locations = get_nav_menu_locations();
435 | $locations['primary'] = $this->menu_id;
436 | $locations['secondary'] = $secondary_id;
437 | set_theme_mod( 'nav_menu_locations', $locations );
438 |
439 | wp_set_current_user( self::$admin_id );
440 |
441 | $request = new WP_REST_Request( 'POST', '/wp/v2/menus/' . $this->menu_id );
442 | $request->set_body_params(
443 | array(
444 | 'locations' => array( 'secondary' ),
445 | )
446 | );
447 | $response = rest_get_server()->dispatch( $request );
448 |
449 | $this->assertEquals( 200, $response->get_status() );
450 |
451 | $locations = get_nav_menu_locations();
452 | $this->assertArrayNotHasKey( 'primary', $locations );
453 | $this->assertArrayHasKey( 'secondary', $locations );
454 | $this->assertEquals( $this->menu_id, $locations['secondary'] );
455 | }
456 |
457 | /**
458 | *
459 | */
460 | public function test_get_items_no_permission() {
461 | wp_set_current_user( 0 );
462 | $request = new WP_REST_Request( 'GET', '/wp/v2/menus' );
463 | $response = rest_get_server()->dispatch( $request );
464 | $this->assertErrorResponse( 'rest_cannot_view', $response, 401 );
465 | }
466 |
467 | /**
468 | *
469 | */
470 | public function test_get_item_no_permission() {
471 | wp_set_current_user( 0 );
472 | $request = new WP_REST_Request( 'GET', '/wp/v2/menus/' . $this->menu_id );
473 | $response = rest_get_server()->dispatch( $request );
474 | $this->assertErrorResponse( 'rest_cannot_view', $response, 401 );
475 | }
476 |
477 | /**
478 | *
479 | */
480 | public function test_get_items_wrong_permission() {
481 | wp_set_current_user( self::$subscriber_id );
482 | $request = new WP_REST_Request( 'GET', '/wp/v2/menus' );
483 | $response = rest_get_server()->dispatch( $request );
484 | $this->assertErrorResponse( 'rest_cannot_view', $response, 403 );
485 | }
486 |
487 | /**
488 | *
489 | */
490 | public function test_get_item_wrong_permission() {
491 | wp_set_current_user( self::$subscriber_id );
492 | $request = new WP_REST_Request( 'GET', '/wp/v2/menus/' . $this->menu_id );
493 | $response = rest_get_server()->dispatch( $request );
494 | $this->assertErrorResponse( 'rest_cannot_view', $response, 403 );
495 | }
496 |
497 | /**
498 | * @param WP_REST_Response $response Response Class.
499 | */
500 | protected function check_get_taxonomy_terms_response( $response ) {
501 | $this->assertEquals( 200, $response->get_status() );
502 | $data = $response->get_data();
503 | $args = array(
504 | 'hide_empty' => false,
505 | );
506 | $tags = get_terms( self::TAXONOMY, $args );
507 | $this->assertEquals( count( $tags ), count( $data ) );
508 | $this->assertEquals( $tags[0]->term_id, $data[0]['id'] );
509 | $this->assertEquals( $tags[0]->name, $data[0]['name'] );
510 | $this->assertEquals( $tags[0]->slug, $data[0]['slug'] );
511 | $this->assertEquals( $tags[0]->description, $data[0]['description'] );
512 | }
513 |
514 | /**
515 | * @param WP_REST_Response $response Response Class.
516 | * @param int $id Term ID.
517 | */
518 | protected function check_get_taxonomy_term_response( $response, $id ) {
519 | $this->assertEquals( 200, $response->get_status() );
520 |
521 | $data = $response->get_data();
522 | $menu = get_term( $id, self::TAXONOMY );
523 | $this->check_taxonomy_term( $menu, $data, $response->get_links() );
524 | }
525 |
526 | /**
527 | * @param WP_Term $term WP_Term object.
528 | * @param array $data Data from REST API.
529 | * @param array $links Array of links.
530 | */
531 | protected function check_taxonomy_term( $term, $data, $links ) {
532 | $this->assertEquals( $term->term_id, $data['id'] );
533 | $this->assertEquals( $term->name, $data['name'] );
534 | $this->assertEquals( $term->slug, $data['slug'] );
535 | $this->assertEquals( $term->description, $data['description'] );
536 | $this->assertFalse( isset( $data['parent'] ) );
537 |
538 | $relations = array(
539 | 'self',
540 | 'collection',
541 | 'about',
542 | 'https://api.w.org/post_type',
543 | );
544 |
545 | if ( ! empty( $data['parent'] ) ) {
546 | $relations[] = 'up';
547 | }
548 |
549 | $this->assertEqualSets( $relations, array_keys( $links ) );
550 | $this->assertContains( 'wp/v2/taxonomies/' . $term->taxonomy, $links['about'][0]['href'] );
551 | $this->assertEquals( add_query_arg( 'menus', $term->term_id, rest_url( 'wp/v2/menu-items' ) ), $links['https://api.w.org/post_type'][0]['href'] );
552 | }
553 |
554 | }
555 |
--------------------------------------------------------------------------------