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 @@