diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist
new file mode 100644
index 0000000..2b20264
--- /dev/null
+++ b/.phpcs.xml.dist
@@ -0,0 +1,32 @@
+
+
+ Generally-applicable sniffs for WordPress plugins.
+
+
+ .
+ /vendor/
+ /node_modules/
+ /tests/
+ /lib/class-wp-rest-widgets-controller.php
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.travis.yml b/.travis.yml
index 427bf71..cb59e2b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,85 +1,60 @@
-# Travis CI Configuration File
+sudo: false
+dist: trusty
-# Tell Travis CI we're using PHP
language: php
-sudo: false
+notifications:
+ email:
+ on_success: never
+ on_failure: change
-matrix:
- include:
- - php: 5.6
- env: WP_TRAVISCI=travis:phpunit WP_VERSION=nightly
- - php: 5.6
- env: WP_TRAVISCI=travis:phpunit WP_VERSION=latest
- - php: 5.6
- env: WP_TRAVISCI=travis:phpvalidate
- - php: 5.5
- env: WP_TRAVISCI=travis:phpunit WP_VERSION=nightly
- - php: 5.4
- env: WP_TRAVISCI=travis:phpunit WP_VERSION=nightly
- - php: 5.3
- env: WP_TRAVISCI=travis:phpunit WP_VERSION=nightly
- - php: 5.2
- env: WP_TRAVISCI=travis:phpunit WP_VERSION=nightly
- - php: hhvm
- env: WP_TRAVISCI=travis:phpunit WP_VERSION=nightly
- - php: 7.0
- env: WP_TRAVISCI=travis:phpunit WP_VERSION=nightly
- allow_failures:
- - php: hhvm
- fast_finish: true
+branches:
+ only:
+ - master
cache:
directories:
- - vendor
- - $HOME/.composer/cache
- - node_modules
+ - $HOME/.composer/cache
-before_install:
- # set up WP install
- - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION
- # prepare for running the tests
- - cd $TRAVIS_BUILD_DIR
- - npm install -g npm
- - npm install -g grunt-cli
- - npm install
- - node --version
- - npm --version
- - grunt --version
+matrix:
+ include:
+ - php: 7.3
+ env: WP_VERSION=latest
+ - php: 7.2
+ env: WP_VERSION=latest
+ - php: 7.1
+ env: WP_VERSION=latest
+ - php: 7.0
+ env: WP_VERSION=latest
+ - php: 5.6
+ env: WP_VERSION=latest
+ - php: 5.6
+ env: WP_VERSION=trunk
+ - php: 5.6
+ env: WP_TRAVISCI=phpcs
+ dist: precise
before_script:
- # Setup Coveralls
+ - export PATH="$HOME/.composer/vendor/bin:$PATH"
+ - composer install
- |
- if [[ "$WP_TRAVISCI" == "travis:phpvalidate" ]] ; then
- composer self-update
- composer install --no-interaction
+ if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then
+ phpenv config-rm xdebug.ini
+ else
+ echo "xdebug.ini does not exist"
fi
- # Setup Coveralls
- |
- if [[ "$WP_TRAVISCI" == "travis:codecoverage" ]] ; then
- composer self-update
- composer install --no-interaction
+ if [[ ! -z "$WP_VERSION" ]] ; then
+ bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION
fi
script:
- - grunt $WP_TRAVISCI
-
-after_script:
- # Push coverage off to Codecov
-- |
- if [[ "$WP_TRAVISCI" == "travis:codecoverage" ]] ; then
- bash <(curl -s https://codecov.io/bash)
- fi
-
-git:
- depth: 1
-
-branches:
- only:
- - master
- - develop
-
-notifications:
- email:
- on_success: never
- on_failure: change
+ - |
+ if [[ ! -z "$WP_VERSION" ]] ; then
+ vendor/bin/phpunit
+ WP_MULTISITE=1 vendor/bin/phpunit
+ fi
+ - |
+ if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then
+ vendor/bin/phpcs
+ fi
diff --git a/README.md b/README.md
index 658af6c..94e2f81 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,27 @@
Feature plugin for Nav Menus and Widgets Endpoints
+
+Endpoints to define for menus:
+
+```
+GET /menus
+POST /menus
+GET /menus/:id
+POST /menus/:id
+DELETE /menus/:id
+```
+
+Endpoints to define for menu items:
+
+```
+GET /menu-items
+POST /menu-items
+GET /menu-items/:id
+POST /menu-items/:id
+DELETE /menu-items/:id
+```
+
Endpoints to define for widgets:
```
@@ -11,4 +32,4 @@ POST /widgets/:type
GET /widget-types
GET /widgets/:type/:number
PUT /widgets/:type/:number
-```
\ No newline at end of file
+```
diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh
index 3cd23a1..5ceac4b 100755
--- a/bin/install-wp-tests.sh
+++ b/bin/install-wp-tests.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
if [ $# -lt 3 ]; then
- echo "usage: $0 [db-host] [wp-version]"
+ echo "usage: $0 [db-host] [wp-version] [skip-database-creation]"
exit 1
fi
@@ -10,9 +10,12 @@ DB_USER=$2
DB_PASS=$3
DB_HOST=${4-localhost}
WP_VERSION=${5-latest}
+SKIP_DB_CREATE=${6-false}
-WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib}
-WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/}
+TMPDIR=${TMPDIR-/tmp}
+TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//")
+WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib}
+WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/}
download() {
if [ `which curl` ]; then
@@ -22,8 +25,19 @@ download() {
fi
}
-if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then
- WP_TESTS_TAG="tags/$WP_VERSION"
+if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then
+ WP_BRANCH=${WP_VERSION%\-*}
+ WP_TESTS_TAG="branches/$WP_BRANCH"
+
+elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
+ WP_TESTS_TAG="branches/$WP_VERSION"
+elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
+ if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
+ # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
+ WP_TESTS_TAG="tags/${WP_VERSION%??}"
+ else
+ WP_TESTS_TAG="tags/$WP_VERSION"
+ fi
elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
WP_TESTS_TAG="trunk"
else
@@ -37,7 +51,6 @@ else
fi
WP_TESTS_TAG="tags/$LATEST_VERSION"
fi
-
set -ex
install_wp() {
@@ -49,31 +62,43 @@ install_wp() {
mkdir -p $WP_CORE_DIR
if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
- mkdir -p /tmp/wordpress-nightly
- download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip
- unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/
- mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR
+ mkdir -p $TMPDIR/wordpress-nightly
+ download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip
+ unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/
+ mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR
else
if [ $WP_VERSION == 'latest' ]; then
local ARCHIVE_NAME='latest'
+ elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then
+ # https serves multiple offers, whereas http serves single.
+ download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json
+ if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
+ # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
+ LATEST_VERSION=${WP_VERSION%??}
+ else
+ # otherwise, scan the releases and get the most up to date minor version of the major release
+ local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'`
+ LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1)
+ fi
+ if [[ -z "$LATEST_VERSION" ]]; then
+ local ARCHIVE_NAME="wordpress-$WP_VERSION"
+ else
+ local ARCHIVE_NAME="wordpress-$LATEST_VERSION"
+ fi
else
local ARCHIVE_NAME="wordpress-$WP_VERSION"
fi
- download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz
- tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR
+ download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz
+ tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR
fi
download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
}
-install_wp_api() {
- git clone https://github.com/WP-API/WP-API.git wp-api
-}
-
install_test_suite() {
# portable in-place argument for both GNU sed and Mac OSX sed
if [[ $(uname -s) == 'Darwin' ]]; then
- local ioption='-i .bak'
+ local ioption='-i.bak'
else
local ioption='-i'
fi
@@ -83,13 +108,14 @@ install_test_suite() {
# set up testing suite
mkdir -p $WP_TESTS_DIR
svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
+ svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
fi
- cd $WP_TESTS_DIR
-
if [ ! -f wp-tests-config.php ]; then
download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
- sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR':" "$WP_TESTS_DIR"/wp-tests-config.php
+ # remove all forward slashes in the end
+ WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::")
+ sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
@@ -99,6 +125,11 @@ install_test_suite() {
}
install_db() {
+
+ if [ ${SKIP_DB_CREATE} = "true" ]; then
+ return 0
+ fi
+
# parse DB_HOST for port or socket references
local PARTS=(${DB_HOST//\:/ })
local DB_HOSTNAME=${PARTS[0]};
@@ -120,6 +151,5 @@ install_db() {
}
install_wp
-install_wp_api
install_test_suite
install_db
diff --git a/composer.json b/composer.json
index 7a2b494..6acd5a2 100644
--- a/composer.json
+++ b/composer.json
@@ -18,8 +18,11 @@
"composer/installers": "~1.0"
},
"require-dev": {
- "squizlabs/php_codesniffer": "2.3.4",
- "wp-coding-standards/wpcs": "0.8.0"
+ "squizlabs/php_codesniffer": "^3.3.1",
+ "wp-coding-standards/wpcs": "^2.1.1",
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0",
+ "phpcompatibility/phpcompatibility-wp": "^2.0",
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"extra": {
"installer-name": "json-rest-api"
diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php
new file mode 100644
index 0000000..d0422a2
--- /dev/null
+++ b/lib/class-wp-rest-menu-items-controller.php
@@ -0,0 +1,730 @@
+get_nav_menu_item( $id );
+ }
+
+ /**
+ * Get the nav menu item, if the ID is valid.
+ *
+ * @param int $id Supplied ID.
+ *
+ * @return object|WP_Error Post object if ID is valid, WP_Error otherwise.
+ */
+ protected function get_nav_menu_item( $id ) {
+ $post = parent::get_post( $id );
+ if ( is_wp_error( $post ) ) {
+ return $post;
+ }
+ $nav_item = wp_setup_nav_menu_item( $post );
+
+ return $nav_item;
+ }
+
+ /**
+ * Creates a single post.
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ *
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function create_item( $request ) {
+ if ( ! empty( $request['id'] ) ) {
+ return new WP_Error( 'rest_post_exists', __( 'Cannot create existing post.' ), array( 'status' => 400 ) );
+ }
+
+ $prepared_nav_item = (array) $this->prepare_item_for_database( $request );
+
+ if ( is_wp_error( $prepared_nav_item ) ) {
+ return $prepared_nav_item;
+ }
+
+ $menu_id = (int) $request['menu_id'];
+ $id = 0;
+
+ $nav_menu_item_id = wp_update_nav_menu_item( $menu_id, $id, $prepared_nav_item );
+
+ if ( is_wp_error( $nav_menu_item_id ) ) {
+ if ( 'db_insert_error' === $nav_menu_item_id->get_error_code() ) {
+ $nav_menu_item_id->add_data( array( 'status' => 500 ) );
+ } else {
+ $nav_menu_item_id->add_data( array( 'status' => 400 ) );
+ }
+
+ return $nav_menu_item_id;
+ }
+
+ $nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
+ if ( is_wp_error( $nav_menu_item ) ) {
+ $nav_menu_item->add_data( array( 'status' => 404 ) );
+
+ return $nav_menu_item;
+ }
+
+ /**
+ * Fires after a single nav menu item is created or updated via the REST API.
+ *
+ * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
+ *
+ * @param object $nav_menu_item Inserted or updated nav item object.
+ * @param WP_REST_Request $request Request object.
+ * @param bool $creating True when creating a post, false when updating.
+ * SA
+ */
+ do_action( "rest_insert_{$this->post_type}", $nav_menu_item, $request, true );
+
+ $schema = $this->get_item_schema();
+
+ if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
+ $meta_update = $this->meta->update_value( $request['meta'], $nav_menu_item_id );
+
+ if ( is_wp_error( $meta_update ) ) {
+ return $meta_update;
+ }
+ }
+
+ $nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
+ $fields_update = $this->update_additional_fields_for_object( $nav_menu_item, $request );
+
+ if ( is_wp_error( $fields_update ) ) {
+ return $fields_update;
+ }
+
+ $request->set_param( 'context', 'edit' );
+
+ /**
+ * Fires after a single nav menu item is completely created or updated via the REST API.
+ *
+ * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
+ *
+ * @param object $nav_menu_item Inserted or updated nav item object.
+ * @param WP_REST_Request $request Request object.
+ * @param bool $creating True when creating a post, false when updating.
+ */
+ do_action( "rest_after_insert_{$this->post_type}", $nav_menu_item, $request, true );
+
+ $response = $this->prepare_item_for_response( $nav_menu_item, $request );
+ $response = rest_ensure_response( $response );
+
+ $response->set_status( 201 );
+ $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $nav_menu_item_id ) ) );
+
+ return $response;
+ }
+
+ /**
+ * Updates a single nav menu item.
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ *
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function update_item( $request ) {
+ $valid_check = $this->get_nav_menu_item( $request['id'] );
+ if ( is_wp_error( $valid_check ) ) {
+ return $valid_check;
+ }
+
+ $prepared_nav_item = $this->prepare_item_for_database( $request );
+
+ if ( is_wp_error( $prepared_nav_item ) ) {
+ return $prepared_nav_item;
+ }
+
+ $menu_id = (int) $request['menu_id'];
+
+ $nav_menu_item_id = wp_update_nav_menu_item( $menu_id, $request['id'], $prepared_nav_item );
+
+ if ( is_wp_error( $nav_menu_item_id ) ) {
+ if ( 'db_update_error' === $nav_menu_item_id->get_error_code() ) {
+ $nav_menu_item_id->add_data( array( 'status' => 500 ) );
+ } else {
+ $nav_menu_item_id->add_data( array( 'status' => 400 ) );
+ }
+
+ return $nav_menu_item_id;
+ }
+
+ $nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
+ if ( is_wp_error( $nav_menu_item ) ) {
+ $nav_menu_item->add_data( array( 'status' => 404 ) );
+
+ return $nav_menu_item;
+ }
+
+ /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
+ do_action( "rest_insert_{$this->post_type}", $nav_menu_item, $request, false );
+
+ $schema = $this->get_item_schema();
+
+ if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
+ $meta_update = $this->meta->update_value( $request['meta'], $nav_menu_item->ID );
+
+ if ( is_wp_error( $meta_update ) ) {
+ return $meta_update;
+ }
+ }
+
+ $nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
+ $fields_update = $this->update_additional_fields_for_object( $nav_menu_item, $request );
+
+ if ( is_wp_error( $fields_update ) ) {
+ return $fields_update;
+ }
+
+ $request->set_param( 'context', 'edit' );
+
+ /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
+ do_action( "rest_after_insert_{$this->post_type}", $nav_menu_item, $request, false );
+
+ $response = $this->prepare_item_for_response( $nav_menu_item, $request );
+
+ return rest_ensure_response( $response );
+ }
+
+ /**
+ * Prepares a single post for create or update.
+ *
+ * @param WP_REST_Request $request Request object.
+ *
+ * @return stdClass
+ */
+ protected function prepare_item_for_database( $request ) {
+ $prepared_nav_item = array(
+ 'menu-item-db-id' => 0,
+ 'menu-item-object-id' => 0,
+ 'menu-item-object' => '',
+ 'menu-item-parent-id' => 0,
+ 'menu-item-position' => 0,
+ 'menu-item-type' => 'custom',
+ 'menu-item-title' => '',
+ 'menu-item-url' => '',
+ 'menu-item-description' => '',
+ 'menu-item-attr-title' => '',
+ 'menu-item-target' => '',
+ 'menu-item-classes' => '',
+ 'menu-item-xfn' => '',
+ 'menu-item-status' => 'publish',
+ );
+
+ $mapping = array(
+ 'menu-item-db-id' => 'db_id',
+ 'menu-item-object-id' => 'object_id',
+ 'menu-item-object' => 'object',
+ 'menu-item-parent-id' => 'menu_item_parent',
+ 'menu-item-position' => 'menu_order',
+ 'menu-item-type' => 'type',
+ 'menu-item-url' => 'url',
+ 'menu-item-description' => 'description',
+ 'menu-item-attr-title' => 'attr_title',
+ 'menu-item-target' => 'target',
+ 'menu-item-classes' => 'classes',
+ 'menu-item-xfn' => 'xfn',
+ 'menu-item-status' => 'status',
+ );
+
+ $schema = $this->get_item_schema();
+
+ foreach ( $mapping as $original => $api_request ) {
+ if ( ! empty( $schema['properties'][ $api_request ] ) && isset( $request[ $api_request ] ) ) {
+ $prepared_nav_item[ $original ] = rest_sanitize_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] );
+ }
+ }
+
+ // Nav menu title.
+ if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
+ if ( is_string( $request['title'] ) ) {
+ $prepared_nav_item['menu-item-title'] = $request['title'];
+ } elseif ( ! empty( $request['title']['raw'] ) ) {
+ $prepared_nav_item['menu-item-title'] = $request['title']['raw'];
+ }
+ }
+
+ if ( ! $prepared_nav_item['menu-item-object'] && $prepared_nav_item['menu-item-object-id'] ) {
+ if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) {
+ $original = get_term( (int) $prepared_nav_item['menu-item-object-id'] );
+ if ( empty( $original ) ) {
+ return new WP_Error( 'rest_term_invalid_id', __( 'Invalid term ID.' ), array( 'status' => 400 ) );
+ }
+ $prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original );
+ } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) {
+ $original = get_post( (int) $prepared_nav_item['menu-item-object-id'] );
+ if ( empty( $original ) ) {
+ return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 400 ) );
+ }
+ $prepared_nav_item['menu-item-object'] = get_post_type( $original );
+ }
+ }
+
+ $prepared_nav_item['menu-item-classes'] = implode( ' ', array_map( 'sanitize_html_class', $prepared_nav_item['menu-item-classes'] ) );
+ $prepared_nav_item['menu-item-xfn'] = implode( ' ', array_map( 'sanitize_html_class', $prepared_nav_item['menu-item-xfn'] ) );
+
+ $prepared_nav_item = (object) $prepared_nav_item;
+
+ /**
+ * Filters a post before it is inserted via the REST API.
+ *
+ * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
+ *
+ * @param stdClass $prepared_post An object representing a single post prepared
+ * for inserting or updating the database.
+ * @param WP_REST_Request $request Request object.
+ */
+ return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_nav_item, $request );
+ }
+
+ /**
+ * Prepares a single post output for response.
+ *
+ * @param object $post Post object.
+ * @param WP_REST_Request $request Request object.
+ *
+ * @return WP_REST_Response Response object.
+ */
+ public function prepare_item_for_response( $post, $request ) {
+ $fields = $this->get_fields_for_response( $request );
+
+ // Base fields for every post.
+ $menu_item = wp_setup_nav_menu_item( $post );
+
+ if ( in_array( 'id', $fields, true ) ) {
+ $data['id'] = $menu_item->ID;
+ }
+
+ if ( in_array( 'title', $fields, true ) ) {
+ add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
+
+ $data['title'] = array(
+ 'raw' => $menu_item->post_title,
+ 'rendered' => $menu_item->title,
+ );
+
+ remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
+ }
+
+ if ( in_array( 'status', $fields, true ) ) {
+ $data['status'] = $menu_item->post_status;
+ }
+
+ if ( in_array( 'url', $fields, true ) ) {
+ $data['url'] = $menu_item->url;
+ }
+
+ if ( in_array( 'attr_title', $fields, true ) ) {
+ $data['attr_title'] = $menu_item->attr_title; // Same as post_excerpt.
+ }
+
+ if ( in_array( 'description', $fields, true ) ) {
+ $data['description'] = $menu_item->description; // Same as post_content.
+ }
+
+ if ( in_array( 'type', $fields, true ) ) {
+ $data['type'] = $menu_item->type; // Using 'item_type' since 'type' already exists.
+ }
+ if ( in_array( 'type_label', $fields, true ) ) {
+ $data['type_label'] = $menu_item->type_label; // Using 'item_type_label' to match up with 'item_type' - IS READ ONLY!
+ }
+
+ if ( in_array( 'object', $fields, true ) ) {
+ $data['object'] = $menu_item->object;
+ }
+
+ if ( in_array( 'object_id', $fields, true ) ) {
+ $data['object_id'] = absint( $menu_item->object_id ); // Usually is a string, but lets expose as an integer.
+ }
+
+ if ( in_array( 'parent', $fields, true ) ) {
+ $data['parent'] = absint( $menu_item->post_parent ); // Same as post_parent, expose as integer.
+ }
+
+ if ( in_array( 'menu_item_parent', $fields, true ) ) {
+ $data['menu_item_parent'] = absint( $menu_item->menu_item_parent ); // Same as post_parent, expose as integer.
+ }
+
+ if ( in_array( 'menu_order', $fields, true ) ) {
+ $data['menu_order'] = absint( $menu_item->menu_order ); // Same as post_parent, expose as integer.
+ }
+
+ if ( in_array( 'target', $fields, true ) ) {
+ $data['target'] = $menu_item->target;
+ }
+
+ if ( in_array( 'classes', $fields, true ) ) {
+ $data['classes'] = (array) $menu_item->classes;
+ }
+
+ if ( in_array( 'xfn', $fields, true ) ) {
+ $data['xfn'] = (array) $menu_item->xfn;
+ }
+
+ if ( in_array( 'meta', $fields, true ) ) {
+ $data['meta'] = $this->meta->get_value( $menu_item->ID, $request );
+ }
+
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+ $data = $this->add_additional_fields_to_object( $data, $request );
+ $data = $this->filter_response_by_context( $data, $context );
+
+ // Wrap the data in a response object.
+ $response = rest_ensure_response( $data );
+
+ $links = $this->prepare_links( $menu_item );
+ $response->add_links( $links );
+
+ if ( ! empty( $links['self']['href'] ) ) {
+ $actions = $this->get_available_actions( $menu_item, $request );
+
+ $self = $links['self']['href'];
+
+ foreach ( $actions as $rel ) {
+ $response->add_link( $rel, $self );
+ }
+ }
+
+ /**
+ * Filters the post data for a response.
+ *
+ * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
+ *
+ * @param WP_REST_Response $response The response object.
+ * @param object $post Post object.
+ * @param WP_REST_Request $request Request object.
+ */
+ return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request );
+ }
+
+ /**
+ * Prepares links for the request.
+ *
+ * @param object $menu_item Menu object.
+ *
+ * @return array Links for the given post.
+ */
+ protected function prepare_links( $menu_item ) {
+ $links = parent::prepare_links( $menu_item );
+
+ if ( 'post_type' === $menu_item->type && ! empty( $menu_item->object_id ) ) {
+ $post_type_object = get_post_type_object( $menu_item->object );
+ if ( $post_type_object->show_in_rest ) {
+ $rest_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
+ $url = rest_url( sprintf( 'wp/v2/%s/%d', $rest_base, $menu_item->object_id ) );
+ $links['https://api.w.org/object'][] = array(
+ 'href' => $url,
+ 'post_type' => $menu_item->type,
+ 'embeddable' => true,
+ );
+ }
+ } elseif ( 'taxonomy' === $menu_item->type && ! empty( $menu_item->object_id ) ) {
+ $taxonomy_object = get_taxonomy( $menu_item->object );
+ if ( $taxonomy_object->show_in_rest ) {
+ $rest_base = ! empty( $taxonomy_object->rest_base ) ? $taxonomy_object->rest_base : $taxonomy_object->name;
+ $url = rest_url( sprintf( 'wp/v2/%s/%d', $rest_base, $menu_item->object_id ) );
+ $links['https://api.w.org/object'][] = array(
+ 'href' => $url,
+ 'taxonomy' => $menu_item->type,
+ 'embeddable' => true,
+ );
+ }
+ }
+
+ return $links;
+ }
+
+ /**
+ * Retrieve Link Description Objects that should be added to the Schema for the posts collection.
+ *
+ * @since 4.9.8
+ *
+ * @return array
+ */
+ protected function get_schema_links() {
+ $links = parent::get_schema_links();
+ $href = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" );
+ $links[] = array(
+ 'rel' => 'https://api.w.org/object',
+ 'title' => __( 'Get linked object.' ),
+ 'href' => $href,
+ 'targetSchema' => array(
+ 'type' => 'object',
+ 'properties' => array(
+ 'object' => array(
+ 'type' => 'integer',
+ ),
+ ),
+ ),
+ );
+
+ return $links;
+ }
+
+ /**
+ * Retrieves the term's schema, conforming to JSON Schema.
+ *
+ * @return array Item schema data.
+ */
+ public function get_item_schema() {
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => $this->post_type,
+ 'type' => 'object',
+ );
+
+ $schema['properties']['title'] = array(
+ 'description' => __( 'The title for the object.' ),
+ 'type' => 'object',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ 'arg_options' => array(
+ 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
+ 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
+ ),
+ 'properties' => array(
+ 'raw' => array(
+ 'description' => __( 'Title for the object, as it exists in the database.' ),
+ 'type' => 'string',
+ 'context' => array( 'edit' ),
+ ),
+ 'rendered' => array(
+ 'description' => __( 'HTML title for the object, transformed for display.' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ 'readonly' => true,
+ ),
+ ),
+ );
+
+ $schema['properties']['id'] = array(
+ 'description' => __( 'Unique identifier for the object.' ),
+ 'type' => 'integer',
+ 'default' => 0,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ 'readonly' => true,
+ );
+
+ $schema['properties']['menu_id'] = array(
+ 'description' => __( 'Unique identifier for the menu.' ),
+ 'type' => 'integer',
+ 'context' => array( 'edit' ),
+ 'default' => 0,
+ );
+
+ $schema['properties']['type_label'] = array(
+ 'description' => __( 'Name of type.' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'embed' ),
+ 'readonly' => true,
+ );
+
+ $schema['properties']['type'] = array(
+ 'description' => __( 'Type of menu item' ),
+ 'type' => 'string',
+ 'enum' => array( 'taxonomy', 'post_type', 'post_type_archive', 'custom' ),
+ 'context' => array( 'view', 'edit', 'embed' ),
+ );
+
+ $schema['properties']['status'] = array(
+ 'description' => __( 'A named status for the object.' ),
+ 'type' => 'string',
+ 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ),
+ 'context' => array( 'view', 'edit' ),
+ );
+
+ $schema['properties']['link'] = array(
+ 'description' => __( 'URL to the object.' ),
+ 'type' => 'string',
+ 'format' => 'uri',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ 'readonly' => true,
+ );
+
+ $schema['properties']['parent'] = array(
+ 'description' => __( 'The ID for the parent of the object.' ),
+ 'type' => 'integer',
+ 'context' => array( 'view', 'edit' ),
+ );
+
+ $schema['properties']['attr_title'] = array(
+ 'description' => __( 'The title attribute of the link element for this menu item .' ),
+ 'context' => array( 'view', 'edit' ),
+ 'type' => 'string',
+ );
+ $schema['properties']['classes'] = array(
+ 'description' => __( 'The array of class attribute values for the link element of this menu item .' ),
+ 'context' => array( 'view', 'edit' ),
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string',
+ ),
+ );
+
+ $schema['properties']['db_id'] = array(
+ 'description' => __( 'The DB ID of this item as a nav_menu_item object, if it exists( 0 if it doesn\'t exist).' ),
+ 'context' => array( 'view', 'edit' ),
+ 'type' => 'integer',
+ );
+
+ $schema['properties']['description'] = array(
+ 'description' => __( 'The description of this menu item.' ),
+ 'context' => array( 'view', 'edit' ),
+ 'type' => 'string',
+ );
+
+ $schema['properties']['menu_item_parent'] = array(
+ 'description' => __( 'The DB ID of the nav_menu_item that is this item\'s menu parent, if any . 0 otherwise . ' ),
+ 'context' => array( 'view', 'edit' ),
+ 'type' => 'integer',
+ );
+
+ $schema['properties']['menu_order'] = array(
+ 'description' => __( 'The DB ID of the nav_menu_item that is this item\'s menu parent, if any . 0 otherwise . ' ),
+ 'context' => array( 'view', 'edit' ),
+ 'type' => 'integer',
+ );
+ $schema['properties']['object'] = array(
+ 'description' => __( 'The type of object originally represented, such as "category," "post", or "attachment."' ),
+ 'context' => array( 'view', 'edit' ),
+ );
+
+ $schema['properties']['object_id'] = array(
+ 'description' => __( 'The DB ID of the original object this menu item represents, e . g . ID for posts and term_id for categories .' ),
+ 'context' => array( 'view', 'edit' ),
+ 'type' => 'integer',
+ );
+
+ $schema['properties']['target'] = array(
+ 'description' => __( 'The target attribute of the link element for this menu item . The family of objects originally represented, such as "post_type" or "taxonomy."' ),
+ 'context' => array( 'view', 'edit' ),
+ 'type' => 'string',
+ );
+
+ $schema['properties']['type_label'] = array(
+ 'description' => __( 'The singular label used to describe this type of menu item.' ),
+ 'context' => array( 'view' ),
+ 'type' => 'string',
+ 'readonly' => true,
+ );
+
+ $schema['properties']['url'] = array(
+ 'description' => __( 'The URL to which this menu item points .' ),
+ 'type' => 'string',
+ 'format' => 'uri',
+ 'context' => array( 'view', 'edit' ),
+ );
+
+ $schema['properties']['xfn'] = array(
+ 'description' => __( 'The XFN relationship expressed in the link of this menu item . ' ),
+ 'context' => array( 'view', 'edit' ),
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string',
+ ),
+ );
+
+ $schema['properties']['_invalid'] = array(
+ 'description' => __( ' Whether the menu item represents an object that no longer exists .' ),
+ 'context' => array( 'view', 'edit' ),
+ 'type' => 'boolean',
+ );
+
+ $schema['properties']['meta'] = $this->meta->get_field_schema();
+
+ $schema_links = $this->get_schema_links();
+
+ if ( $schema_links ) {
+ $schema['links'] = $schema_links;
+ }
+
+ return $this->add_additional_fields_schema( $schema );
+ }
+
+ /**
+ * Retrieves the query params for the posts collection.
+ *
+ * @return array Collection parameters.
+ */
+ public function get_collection_params() {
+ $query_params = parent::get_collection_params();
+
+ $query_params['menu_order'] = array(
+ 'description' => __( 'Limit result set to posts with a specific menu_order value.' ),
+ 'type' => 'integer',
+ );
+
+ $query_params['order'] = array(
+ 'description' => __( 'Order sort attribute ascending or descending.' ),
+ 'type' => 'string',
+ 'default' => 'asc',
+ 'enum' => array( 'asc', 'desc' ),
+ );
+
+ $query_params['orderby'] = array(
+ 'description' => __( 'Sort collection by object attribute.' ),
+ 'type' => 'string',
+ 'default' => 'menu_order',
+ 'enum' => array(
+ 'author',
+ 'date',
+ 'id',
+ 'include',
+ 'modified',
+ 'parent',
+ 'relevance',
+ 'slug',
+ 'include_slugs',
+ 'title',
+ 'menu_order',
+ ),
+ );
+
+ return $query_params;
+ }
+
+ /**
+ * Determines the allowed query_vars for a get_items() response and prepares
+ * them for WP_Query.
+ *
+ * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array.
+ * @param WP_REST_Request $request Optional. Full details about the request.
+ *
+ * @return array Items query arguments.
+ */
+ protected function prepare_items_query( $prepared_args = array(), $request = null ) {
+ $query_args = parent::prepare_items_query( $prepared_args, $request );
+
+ // Map to proper WP_Query orderby param.
+ if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) {
+ $orderby_mappings = array(
+ 'id' => 'ID',
+ 'include' => 'post__in',
+ 'slug' => 'post_name',
+ 'include_slugs' => 'post_name__in',
+ 'menu_order' => 'menu_order',
+ );
+
+ if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
+ $query_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
+ }
+ }
+
+ return $query_args;
+ }
+}
diff --git a/lib/class-wp-rest-menus-controller.php b/lib/class-wp-rest-menus-controller.php
new file mode 100644
index 0000000..4bd06fd
--- /dev/null
+++ b/lib/class-wp-rest-menus-controller.php
@@ -0,0 +1,296 @@
+get_item_schema();
+ if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
+ $response['menu-name'] = $request['name'];
+ }
+
+ return $prepared_term;
+ }
+
+ /**
+ * Creates a single term in a taxonomy.
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ *
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function create_item( $request ) {
+ if ( isset( $request['parent'] ) ) {
+ if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
+ return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Cannot set parent term, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
+ }
+
+ $parent = wp_get_nav_menu_object( (int) $request['parent'] );
+
+ if ( ! $parent ) {
+ return new WP_Error( 'rest_term_invalid', __( 'Parent term does not exist.' ), array( 'status' => 400 ) );
+ }
+ }
+
+ $prepared_term = $this->prepare_item_for_database( $request );
+
+ $term = wp_update_nav_menu_object( 0, wp_slash( (array) $prepared_term ) );
+
+ if ( is_wp_error( $term ) ) {
+ /*
+ * If we're going to inform the client that the term already exists,
+ * give them the identifier for future use.
+ */
+ $term_id = $term->get_error_data( 'term_exists' );
+ if ( $term_id ) {
+ $existing_term = get_term( $term_id, $this->taxonomy );
+ $term->add_data( $existing_term->term_id, 'term_exists' );
+ $term->add_data(
+ array(
+ 'status' => 400,
+ 'term_id' => $term_id,
+ )
+ );
+ }
+
+ return $term;
+ }
+
+ $term = $this->get_term( $term );
+
+ /**
+ * Fires after a single term is created or updated via the REST API.
+ *
+ * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
+ *
+ * @param WP_Term $term Inserted or updated term object.
+ * @param WP_REST_Request $request Request object.
+ * @param bool $creating True when creating a term, false when updating.
+ */
+ do_action( "rest_insert_{$this->taxonomy}", $term, $request, true );
+
+ $schema = $this->get_item_schema();
+ if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
+ $meta_update = $this->meta->update_value( $request['meta'], $term->term_id );
+
+ if ( is_wp_error( $meta_update ) ) {
+ return $meta_update;
+ }
+ }
+
+ $fields_update = $this->update_additional_fields_for_object( $term, $request );
+
+ if ( is_wp_error( $fields_update ) ) {
+ return $fields_update;
+ }
+
+ $request->set_param( 'context', 'view' );
+
+ /**
+ * Fires after a single term is completely created or updated via the REST API.
+ *
+ * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
+ *
+ * @param WP_Term $term Inserted or updated term object.
+ * @param WP_REST_Request $request Request object.
+ * @param bool $creating True when creating a term, false when updating.
+ *
+ * @since 5.0.0
+ */
+ do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, true );
+
+ $response = $this->prepare_item_for_response( $term, $request );
+ $response = rest_ensure_response( $response );
+
+ $response->set_status( 201 );
+ $response->header( 'Location', rest_url( $this->namespace . '/' . $this->rest_base . '/' . $term->term_id ) );
+
+ return $response;
+ }
+
+ /**
+ * Updates a single term from a taxonomy.
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ *
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function update_item( $request ) {
+ $term = $this->get_term( $request['id'] );
+ if ( is_wp_error( $term ) ) {
+ return $term;
+ }
+
+ if ( isset( $request['parent'] ) ) {
+ if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
+ return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Cannot set parent term, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
+ }
+
+ $parent = get_term( (int) $request['parent'], $this->taxonomy );
+
+ if ( ! $parent ) {
+ return new WP_Error( 'rest_term_invalid', __( 'Parent term does not exist.' ), array( 'status' => 400 ) );
+ }
+ }
+
+ $prepared_term = $this->prepare_item_for_database( $request );
+
+ // Only update the term if we haz something to update.
+ if ( ! empty( $prepared_term ) ) {
+ $update = wp_update_nav_menu_object( $term->term_id, wp_slash( (array) $prepared_term ) );
+
+ if ( is_wp_error( $update ) ) {
+ return $update;
+ }
+ }
+
+ $term = get_term( $term->term_id, $this->taxonomy );
+
+ /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
+ do_action( "rest_insert_{$this->taxonomy}", $term, $request, false );
+
+ $schema = $this->get_item_schema();
+ if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
+ $meta_update = $this->meta->update_value( $request['meta'], $term->term_id );
+
+ if ( is_wp_error( $meta_update ) ) {
+ return $meta_update;
+ }
+ }
+
+ $fields_update = $this->update_additional_fields_for_object( $term, $request );
+
+ if ( is_wp_error( $fields_update ) ) {
+ return $fields_update;
+ }
+
+ $request->set_param( 'context', 'view' );
+
+ /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
+ do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, false );
+
+ $response = $this->prepare_item_for_response( $term, $request );
+
+ return rest_ensure_response( $response );
+ }
+
+ /**
+ * Deletes a single term from a taxonomy.
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ *
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function delete_item( $request ) {
+ $term = $this->get_term( $request['id'] );
+ if ( is_wp_error( $term ) ) {
+ return $term;
+ }
+
+ $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
+
+ // We don't support trashing for terms.
+ if ( ! $force ) {
+ /* translators: %s: force=true */
+ return new WP_Error( 'rest_trash_not_supported', sprintf( __( "Terms do not support trashing. Set '%s' to delete." ), 'force=true' ), array( 'status' => 501 ) );
+ }
+
+ $request->set_param( 'context', 'view' );
+
+ $previous = $this->prepare_item_for_response( $term, $request );
+
+ $retval = wp_delete_nav_menu( $term );
+
+ if ( ! $retval ) {
+ return new WP_Error( 'rest_cannot_delete', __( 'The term cannot be deleted.' ), array( 'status' => 500 ) );
+ }
+
+ $response = new WP_REST_Response();
+ $response->set_data(
+ array(
+ 'deleted' => true,
+ 'previous' => $previous->get_data(),
+ )
+ );
+
+ /**
+ * Fires after a single term is deleted via the REST API.
+ *
+ * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
+ *
+ * @param WP_Term $term The deleted term.
+ * @param WP_REST_Response $response The response data.
+ * @param WP_REST_Request $request The request sent to the API.
+ */
+ do_action( "rest_delete_{$this->taxonomy}", $term, $response, $request );
+
+ return $response;
+ }
+
+ /**
+ * Retrieves the term's schema, conforming to JSON Schema.
+ *
+ * @return array Item schema data.
+ */
+ public function get_item_schema() {
+ $schema = parent::get_item_schema();
+ unset( $schema['properties']['count'] );
+ unset( $schema['properties']['link'] );
+ unset( $schema['properties']['taxonomy'] );
+
+ return $schema;
+ }
+}
diff --git a/lib/class-wp-rest-nav-menu-items-controller.php b/lib/class-wp-rest-nav-menu-items-controller.php
deleted file mode 100644
index a22012f..0000000
--- a/lib/class-wp-rest-nav-menu-items-controller.php
+++ /dev/null
@@ -1,52 +0,0 @@
-namespace = 'wp/v2';
- $this->rest_base = 'nav-menu-items';
- }
-
- public function register_routes() {
- // @todo
- }
-
- public function get_items_permissions_check( $request ) {
-
- }
-
- public function get_items( $request ) {
-
- }
-
- public function get_item_permissions_check( $request ) {
-
- }
-
- public function get_item( $request ) {
-
- }
-
- public function delete_item_permission_check( $request ) {
-
- }
-
- public function delete_item( $request ) {
-
- }
-
- public function prepare_item_for_response( $item, $request ) {
-
- }
-
- public function get_item_schema() {
-
- }
-
- public function get_collection_params() {
-
- }
-}
diff --git a/lib/class-wp-rest-nav-menus-controller.php b/lib/class-wp-rest-nav-menus-controller.php
deleted file mode 100644
index c2fbaa6..0000000
--- a/lib/class-wp-rest-nav-menus-controller.php
+++ /dev/null
@@ -1,68 +0,0 @@
-namespace = 'wp/v2';
- $this->rest_base = 'nav-menus';
- }
-
- public function register_routes() {
- // @todo
-
- register_rest_route( $this->namespace, '/' . $this->rest_base, array(
- array(
- 'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_items' ),
- 'permission_callback' => array( $this, 'get_items_permissions_check' ),
- 'args' => $this->get_collection_params(),
- ),
- 'schema' => array( $this, 'get_public_item_schema' ),
- ) );
-
- }
-
- public function get_items_permissions_check( $request ) {
-
- return true;
- }
-
- public function get_items( $request ) {
-
- $nav_menu_items = array();
- $response = rest_ensure_response( $nav_menu_items );
- return $response;
-
- }
-
- public function get_item_permissions_check( $request ) {
-
- }
-
- public function get_item( $request ) {
-
- }
-
- public function delete_item_permission_check( $request ) {
-
- }
-
- public function delete_item( $request ) {
-
- }
-
- public function prepare_item_for_response( $item, $request ) {
-
- }
-
- public function get_item_schema() {
-
- }
-
- public function get_collection_params() {
-
- }
-}
diff --git a/lib/class-wp-rest-widgets-controller.php b/lib/class-wp-rest-widgets-controller.php
index ee33b7f..36e1c4a 100644
--- a/lib/class-wp-rest-widgets-controller.php
+++ b/lib/class-wp-rest-widgets-controller.php
@@ -31,7 +31,7 @@ class WP_REST_Widgets_Controller extends WP_REST_Controller {
public function __construct( $widgets ) {
$this->namespace = 'wp/v2';
$this->rest_base = 'widgets';
- $this->widgets = $widgets;
+ $this->widgets = $widgets;
$this->sidebars = wp_get_sidebars_widgets();
@@ -41,90 +41,110 @@ public function __construct( $widgets ) {
public function register_routes() {
// /wp/v2/widgets
- register_rest_route( $this->namespace, '/' . $this->rest_base, array(
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base,
array(
- 'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_items' ),
- 'permission_callback' => array( $this, 'get_items_permissions_check' ),
- 'args' => $this->get_collection_params(),
- ),
- array(
- 'methods' => WP_REST_Server::CREATABLE,
- 'callback' => array( $this, 'create_item' ),
- 'permission_callback' => array( $this, 'create_item_permissions_check' ),
- 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
- ),
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_items' ),
+ 'permission_callback' => array( $this, 'get_items_permissions_check' ),
+ 'args' => $this->get_collection_params(),
+ ),
+ array(
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => array( $this, 'create_item' ),
+ 'permission_callback' => array( $this, 'create_item_permissions_check' ),
+ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
+ ),
- 'schema' => array( $this, 'get_public_items_schema' ),
- ) );
+ 'schema' => array( $this, 'get_public_items_schema' ),
+ )
+ );
// /wp/v2/widgets/:id_base
- register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[^/]+)', array(
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/(?P[^/]+)',
array(
- 'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_item' ),
- 'permission_callback' => array( $this, 'get_item_permissions_check' ),
- 'args' => array(
- 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_item' ),
+ 'permission_callback' => array( $this, 'get_item_permissions_check' ),
+ 'args' => array(
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+ ),
),
- ),
- array(
- 'methods' => WP_REST_Server::EDITABLE,
- 'callback' => array( $this, 'update_item' ),
- 'permission_callback' => array( $this, 'update_item_permissions_check' ),
- 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
- ),
- array(
- 'methods' => WP_REST_Server::DELETABLE,
- 'callback' => array( $this, 'delete_item' ),
- 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
- ),
- 'schema' => array( $this, 'get_public_items_schema' ),
- ) );
+ array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( $this, 'update_item' ),
+ 'permission_callback' => array( $this, 'update_item_permissions_check' ),
+ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::DELETABLE,
+ 'callback' => array( $this, 'delete_item' ),
+ 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
+ ),
+ 'schema' => array( $this, 'get_public_items_schema' ),
+ )
+ );
// /wp/v2/widgets/:id_base/:number
- register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[^/]+)/(?P[\d]+)', array(
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/(?P[^/]+)/(?P[\d]+)',
array(
- 'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_item' ),
- 'permission_callback' => array( $this, 'get_item_permissions_check' ),
- 'args' => array(
- 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_item' ),
+ 'permission_callback' => array( $this, 'get_item_permissions_check' ),
+ 'args' => array(
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+ ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( $this, 'update_item' ),
+ 'permission_callback' => array( $this, 'update_item_permissions_check' ),
+ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::DELETABLE,
+ 'callback' => array( $this, 'delete_item' ),
+ 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
),
- ),
- array(
- 'methods' => WP_REST_Server::EDITABLE,
- 'callback' => array( $this, 'update_item' ),
- 'permission_callback' => array( $this, 'update_item_permissions_check' ),
- 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
- ),
- array(
- 'methods' => WP_REST_Server::DELETABLE,
- 'callback' => array( $this, 'delete_item' ),
- 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
- ),
- 'schema' => array( $this, 'get_public_item_schema' ),
- ) );
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
// /wp/v2/widget-types/
- register_rest_route( $this->namespace, '/widget-types', array(
+ register_rest_route(
+ $this->namespace,
+ '/widget-types',
array(
- 'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_types' ),
- 'permission_callback' => array( $this, 'get_types_permissions_check' ),
- ),
- 'schema' => array( $this, 'get_public_item_schema' ),
- ) );
- register_rest_route( $this->namespace, '/' . $this->rest_base .'/types/(?P[\w-]+)', array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_types' ),
+ 'permission_callback' => array( $this, 'get_types_permissions_check' ),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/types/(?P[\w-]+)',
array(
- 'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_type' ),
- 'permission_callback' => array( $this, 'get_types_permissions_check' ),
- ),
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_type' ),
+ 'permission_callback' => array( $this, 'get_types_permissions_check' ),
+ ),
- 'schema' => array( $this, 'get_public_item_schema' ),
- ) );
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
}
public function get_items_permissions_check( $request ) {
@@ -138,15 +158,14 @@ public function get_items_permissions_check( $request ) {
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
-
- foreach( $this->widgets as $widget ) {
+ foreach ( $this->widgets as $widget ) {
$settings = $widget->get_settings();
- foreach( $settings as $key => $values ) {
+ foreach ( $settings as $key => $values ) {
$this->instances[] = array(
- 'id' => $widget->id_base . '-' . $key,
+ 'id' => $widget->id_base . '-' . $key,
'array_index' => $key,
- 'id_base' => $widget->id_base,
- 'settings' => $values,
+ 'id_base' => $widget->id_base,
+ 'settings' => $values,
);
}
}
@@ -155,24 +174,24 @@ public function get_items( $request ) {
return rest_ensure_response( array() );
};
- $args = array();
+ $args = array();
$args['sidebar'] = $request['sidebar'];
// TODO pagination
$instances = array();
- foreach( $this->instances as $instance ) {
- if ( !$this->get_instance_permissions_check( $instance['id'] ) ) {
+ foreach ( $this->instances as $instance ) {
+ if ( ! $this->get_instance_permissions_check( $instance['id'] ) ) {
continue;
}
- if ( !is_null( $args['sidebar'] ) && $args['sidebar'] !== $this->get_instance_sidebar( $instance['id'] ) ) {
+ if ( ! is_null( $args['sidebar'] ) && $args['sidebar'] !== $this->get_instance_sidebar( $instance['id'] ) ) {
continue;
}
- $data = $this->prepare_item_for_response( $instance, $request );
+ $data = $this->prepare_item_for_response( $instance, $request );
$instances[] = $this->prepare_response_for_collection( $data );
}
- if ( !empty( $instances ) && !is_null( $args['sidebar'] ) ) {
+ if ( ! empty( $instances ) && ! is_null( $args['sidebar'] ) ) {
$instances = $this->sort_widgets_by_sidebar_order( $args['sidebar'], $instances );
}
@@ -207,7 +226,7 @@ public function get_instance_permissions_check( $instance_id ) {
* return `wp_inactive_widgets` as sidebar for unassigned widgets
*/
public function get_instance_sidebar( $id ) {
- foreach( $this->sidebars as $sidebar_id => $widgets ) {
+ foreach ( $this->sidebars as $sidebar_id => $widgets ) {
if ( in_array( $id, $widgets ) ) {
return $sidebar_id;
}
@@ -226,13 +245,13 @@ public function get_instance_sidebar( $id ) {
* @return array
*/
public function sort_widgets_by_sidebar_order( $sidebar, $instances ) {
- if ( empty( $this->sidebars[$sidebar] ) ) {
+ if ( empty( $this->sidebars[ $sidebar ] ) ) {
return array();
}
$new_widgets = array();
- foreach( $this->sidebars[$sidebar] as $widget_id ) {
- foreach( $instances as $instance ) {
+ foreach ( $this->sidebars[ $sidebar ] as $widget_id ) {
+ foreach ( $instances as $instance ) {
if ( $widget_id === $instance['id'] ) {
$new_widgets[] = $instance;
break;
@@ -244,7 +263,6 @@ public function sort_widgets_by_sidebar_order( $sidebar, $instances ) {
}
public function get_item( $request ) {
-
}
public function delete_item_permission_check( $request ) {
@@ -252,34 +270,32 @@ public function delete_item_permission_check( $request ) {
}
public function delete_item( $request ) {
-
}
/**
* Prepare a single widget output for response
*
- * @param array $instance Widget instance
+ * @param array $instance Widget instance
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $data
*/
public function prepare_item_for_response( $instance, $request ) {
-
- $values = $instance['settings'];
- $values['id'] = $instance['id'];
+ $values = $instance['settings'];
+ $values['id'] = $instance['id'];
$values['type'] = $instance['id_base'];
$schema = $this->get_type_schema( $instance['id_base'] );
$data = array();
- foreach( $schema['properties'] as $property_id => $property ) {
+ foreach ( $schema['properties'] as $property_id => $property ) {
// TODO check for public visibility of property and run permissions
// check for private properties.
- if ( isset( $values[$property_id] ) && gettype( $values[$property_id] ) === $property['type'] ) {
- $data[$property_id] = $values[$property_id];
+ if ( isset( $values[ $property_id ] ) && gettype( $values[ $property_id ] ) === $property['type'] ) {
+ $data[ $property_id ] = $values[ $property_id ];
} elseif ( isset( $property['default'] ) ) {
- $data[$property_id] = $property['default'];
+ $data[ $property_id ] = $property['default'];
}
}
@@ -298,7 +314,6 @@ public function prepare_item_for_response( $instance, $request ) {
}
public function get_item_schema() {
-
}
/**
@@ -352,7 +367,6 @@ public function get_types( $request ) {
* @return WP_Error|WP_REST_Response
*/
public function get_type( $request ) {
-
if ( empty( $request['type'] ) ) {
return new WP_Error( 'rest_widget_missing_type', __( 'Request missing widget type.' ), array( 'status' => 400 ) );
}
@@ -383,7 +397,6 @@ public function get_types_permissions_check( $request ) {
* @return array $schema
*/
public function get_type_schema( $id_base ) {
-
$widget = null;
foreach ( $this->widgets as $this_widget ) {
if ( $id_base === $this_widget->id_base ) {
@@ -396,13 +409,13 @@ public function get_type_schema( $id_base ) {
}
$properties = array(
- 'id' => array(
+ 'id' => array(
'description' => __( 'Unique identifier for the object.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
- 'type' => array(
+ 'type' => array(
'description' => __( 'Type of Widget for the object.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
@@ -411,112 +424,112 @@ public function get_type_schema( $id_base ) {
);
$core_widget_schemas = array(
- 'archives' => array(
- 'count' => array(
- 'type' => 'boolean',
+ 'archives' => array(
+ 'count' => array(
+ 'type' => 'boolean',
'default' => false,
),
'dropdown' => array(
- 'type' => 'boolean',
+ 'type' => 'boolean',
'default' => false,
),
),
- 'calendar' => array(),
- 'categories' => array(
- 'count' => array(
- 'type' => 'boolean',
+ 'calendar' => array(),
+ 'categories' => array(
+ 'count' => array(
+ 'type' => 'boolean',
'default' => false,
),
'hierarchical' => array(
- 'type' => 'boolean',
+ 'type' => 'boolean',
'default' => false,
),
- 'dropdown' => array(
- 'type' => 'boolean',
+ 'dropdown' => array(
+ 'type' => 'boolean',
'default' => false,
),
),
- 'meta' => array(),
- 'nav_menu' => array(
- 'sortby' => array(
- 'type' => 'string',
+ 'meta' => array(),
+ 'nav_menu' => array(
+ 'sortby' => array(
+ 'type' => 'string',
'default' => 'post_title',
),
'exclude' => array(
- 'type' => 'string',
+ 'type' => 'string',
'default' => '',
),
),
- 'pages' => array(
- 'sortby' => array(
- 'type' => 'string',
+ 'pages' => array(
+ 'sortby' => array(
+ 'type' => 'string',
'default' => 'post_title',
),
'exclude' => array(
- 'type' => 'string',
+ 'type' => 'string',
'default' => '',
),
),
'recent-comments' => array(
'number' => array(
- 'type' => 'integer',
+ 'type' => 'integer',
'default' => 5,
),
),
- 'recent-posts' => array(
- 'number' => array(
- 'type' => 'integer',
+ 'recent-posts' => array(
+ 'number' => array(
+ 'type' => 'integer',
'default' => 5,
),
'show_date' => array(
- 'type' => 'boolean',
+ 'type' => 'boolean',
'default' => false,
),
),
- 'rss' => array(
- 'url' => array(
- 'type' => 'string',
+ 'rss' => array(
+ 'url' => array(
+ 'type' => 'string',
'default' => '',
),
- 'link' => array(
- 'type' => 'string',
+ 'link' => array(
+ 'type' => 'string',
'default' => '',
),
- 'items' => array(
- 'type' => 'integer',
+ 'items' => array(
+ 'type' => 'integer',
'default' => 10,
),
- 'error' => array(
- 'type' => 'string',
+ 'error' => array(
+ 'type' => 'string',
'default' => null,
),
'show_summary' => array(
- 'type' => 'boolean',
+ 'type' => 'boolean',
'default' => false,
),
- 'show_author' => array(
- 'type' => 'boolean',
+ 'show_author' => array(
+ 'type' => 'boolean',
'default' => false,
),
- 'show_date' => array(
- 'type' => 'boolean',
+ 'show_date' => array(
+ 'type' => 'boolean',
'default' => false,
),
),
- 'search' => array(),
- 'tag_cloud' => array(
+ 'search' => array(),
+ 'tag_cloud' => array(
'taxonomy' => array(
- 'type' => 'string',
+ 'type' => 'string',
'default' => 'post_tag',
),
),
- 'text' => array(
- 'text' => array(
- 'type' => 'string',
+ 'text' => array(
+ 'text' => array(
+ 'type' => 'string',
'default' => '',
),
'filter' => array(
- 'type' => 'boolean',
+ 'type' => 'boolean',
'default' => false,
),
),
diff --git a/phpcs.ruleset.xml b/phpcs.ruleset.xml
deleted file mode 100644
index 9fb4760..0000000
--- a/phpcs.ruleset.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
- Sniffs for the coding standards of the WP-API plugin
-
- */core/*
-
-
-
-
-
-
-
-
-
-
-
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index b721cc8..16a3902 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,3 +1,4 @@
+
-
- tests
+ ./tests/
+ ./tests/test-sample.php
-
-
- .
-
-
- ./lib
- ./plugin.php
-
-
diff --git a/plugin.php b/plugin.php
index 1aea915..10032ce 100644
--- a/plugin.php
+++ b/plugin.php
@@ -1,5 +1,7 @@
register_routes();
-
- $nav_menu_item_controller = new WP_REST_Nav_Menu_Items_Controller();
- $nav_menu_item_controller->register_routes();
-
- /**
- * @type WP_Widget_Factory $wp_widget_factory
- */
global $wp_widget_factory;
$widgets_controller = new WP_REST_Widgets_Controller( $wp_widget_factory->widgets );
$widgets_controller->register_routes();
}
+
+
+add_filter( 'register_post_type_args', 'wp_api_nav_menus_widgets_post_type_args', 10, 2 );
+
+/**
+ * Hook in and a post type rest endpoint.
+ *
+ * @param array $args Current registered post type args.
+ * @param string $post_type name of post type.
+ *
+ * @return array
+ */
+function wp_api_nav_menus_widgets_post_type_args( $args, $post_type ) {
+ if ( 'nav_menu_item' === $post_type ) {
+ $args['show_in_rest'] = true;
+ $args['rest_base'] = 'menu-items';
+ $args['rest_controller_class'] = 'WP_REST_Menu_Items_Controller';
+ }
+
+ return $args;
+}
+
+
+add_filter( 'register_taxonomy_args', 'wp_api_nav_menus_widgets_taxonomy_args', 10, 2 );
+
+/**
+ * Hook in and a taxonomy rest endpoint.
+ *
+ * @param array $args Current registered taxonomy args.
+ * @param string $taxonomy name of taxonomy.
+ *
+ * @return array
+ */
+function wp_api_nav_menus_widgets_taxonomy_args( $args, $taxonomy ) {
+ if ( 'nav_menu' === $taxonomy ) {
+ $args['show_in_rest'] = true;
+ $args['rest_base'] = 'menus';
+ $args['rest_controller_class'] = 'WP_REST_Menus_Controller';
+ }
+
+ return $args;
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 5b36916..328a2fd 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -1,56 +1,31 @@