diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af905ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.zip +node_modules/ +node_modules +bower_components/ +vendor/ +unused/ +.sass-cache/ +*.min.css +*.min.js +*-rtl.css diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..ea9ae87 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,26 @@ +{ + "curly": true, + "eqeqeq": true, + "esversion": 6, + "noarg": true, + "nonbsp": true, + "undef": true, + "unused": true, + "strict": true, + "proto": true, + + "browser": true, + "devel": true, + "jquery": true, + + "globals": { + "_": false, + "Backbone": false, + "jQuery": false, + "JSON": false, + "wp": false, + "export": false, + "module": false, + "require": false + } +} diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..140889f --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,523 @@ +/* + * note that some of the tasks defined here may not be used in EVERY project + * I build. + * + * @todo figure out how to call `parcel` (to complile the blocks JS) from grunt + * so that it doesn't have to be called from npm. + */ + +/** + * Extract dependencies from package.json for use in a 'src:' property of a task + * + * @param {object} pkg The parsed package.json + * @returns array + * + * @link https://stackoverflow.com/a/34629499/7751811 + */ +function getDependencies( pkg ) { + 'use strict'; + + if ( ! pkg.hasOwnProperty( 'dependencies' ) ) { + return []; + } + + return Object.keys( pkg.dependencies ).map( function( val ) { + return 'node_modules/' + val + '/**'; + } ); +} + +module.exports = function( grunt ) { + 'use strict'; + + var pkg = grunt.file.readJSON( 'package.json' ); + + // Project configuration. + grunt.initConfig( { + pkg: pkg, + + // cleanup + clean: { + build: [ + '<%= pkg.name %>', '<%= pkg.name %>.zip', 'assets/**/*.min.*', 'assets/css/**/*-rtl.css' + ], + release: [ + '<%= pkg.name %>' + ], + }, + + // minify JS files + uglify: { + build: { + files: [ + { + expand: true, + src: [ + 'assets/js/**/*.js', 'vendor/**/*.js', + '!assets/js/**/*.min.js', '!vendor/**/*.min.js' + ], + dest: '.', + ext: '.min.js', + } + ], + }, + }, + + // create RTL CSS files + rtlcss: { + options: { + // borrowed from Core's Gruntfile.js, with a few mods + // 1. reformated (e.g., [\n\t{ -> [ {, etc) + // 2. dashicon content strings changed from '"\\f140"' + // to "'\\f140'", etc + opts: { + clean: false, + processUrls: { + atrule: true, + decl: false, + }, + stringMap: [ + { + name: 'import-rtl-stylesheet', + priority: 10, + exclusive: true, + search: ['.css'], + replace: ['-rtl.css'], + options: { + scope: 'url', + ignoreCase: false + }, + }, + ], + }, + // @todo grunt-rtlcss appears to require leading tabs in order for the + // "plugin" to find the appropriate lines for change. Look into + // whether there is a config option to change that, in case + // assets/css/wphelpkiticons.css gets committed with spaces. + plugins: [ + { + name: 'swap-dashicons-left-right-arrows', + priority: 10, + directives: { + control: {}, + value: [] + }, + processors: [ + { + expr: /content/im, + action: function( prop, value ) { + if ( value === "'\\f141'" ) { // dashicons-arrow-left + value = "'\\f139'"; + } + else if ( value === "'\\f340'" ) { // dashicons-arrow-left-alt + value = "'\\f344'"; + } + else if ( value === "'\\f341'" ) { // dashicons-arrow-left-alt2 + value = "'\\f345'"; + } + else if ( value === "'\\f139'" ) { // dashicons-arrow-right + value = "'\\f141'"; + } + else if ( value === "'\\f344'" ) { // dashicons-arrow-right-alt + value = "'\\f340'"; + } + else if ( value === "'\\f345'" ) { // dashicons-arrow-right-alt2 + value = "'\\f341'"; + } + + return { + prop: prop, + value: value + }; + }, + } + ], + } + ], + }, + build: { + files: [ + { + expand: true, + src: [ + 'assets/css/**/*.css', 'vendor/**/*.css', + '!assets/css/**/*-rtl.css', '!assets/css/**/*.min.css', + '!vendor/**/*-rtl.css', '!vendor/**/*.min.css' + ], + dest: '.', + ext: '-rtl.css', + } + ], + }, + }, + + // SASS pre-process CSS files + sass: { + options: { + style: 'expanded', + }, + build: { + files: [ + { + expand: true, + src: [ + 'assets/css/**/*.scss' + ], + dest: '.', + ext: '.css', + } + ], + }, + }, + + // minify CSS files + cssmin: { + build: { + files: [ + { + expand: true, + src: [ + 'assets/css/**/*.css', 'vendor/**/*.css', + '!assets/css/**/*.min.css', '!vendor/**/*.min.css', + ], + dest: '.', + ext: '.min.css', + } + ], + }, + }, + + // copy files from one place to another + copy: { + release: { + expand: true, + // installing the shc-framework composer dependency creates vendor/bin + // for some reason even tho it has no binaries...make sure that dir isn't + // included in the release. + src: [ + 'plugin.php', 'readme.txt', 'assets/**', + 'includes/**', 'utils/**', 'vendor/**', + '!assets/css/**/*.scss', + '!vendor/bin', + ], + dest: '<%= pkg.name %>', + }, + node_modules: { + expand: true, + src: getDependencies( pkg ), + dest: 'vendor', + rename: function( dest, src ) { + return dest + '/' + src.substring( src.indexOf( '/' ) + 1 ); + }, + }, + }, + + // package into a zip + zip: { + build: { + expand: true, + cwd: '.', + src: '<%= pkg.name %>/**', + dest: '<%= pkg.name %>.<%= pkg.version %>.zip', + }, + }, + + // do string search/replace on various files + replace: { + namespace: { + src: [ + 'plugin.php', 'uninstall.php', 'includes/**/*.php', + 'vendor/shc/**/*.php', + ], + overwrite: true, + replacements: [ + { + from: /^namespace (.*);$/m, + to: 'namespace <%= pkg.namespace %>;', + } + ], + }, + version_readme_txt: { + src: ['readme.txt'], + overwrite: true, + replacements: [ + { + from: /^(Stable tag:) (.*)/m, + to: '$1 <%= pkg.version %>', + }, + ], + }, + version_plugin: { + src: ['plugin.php'], + overwrite: true, + replacements: [ + // this is for the plugin_data comment + { + from: /^( \* Version:) (.*)/mg, + to: '$1 <%= pkg.version %>', + }, + // this is for plugins that use a static class var + // instead of the dynamic $this->version that SHC Framework allows + { + from: /^(.*static \$VERSION =) '(.*)'/m, + to: "$1 '<%= pkg.version %>'", + }, + // this is for plugins that use a class const + // instead of the dynamic $this->version that SHC Framework allows + { + from: /^(.*const VERSION =) '(.*)'/m, + to: "$1 '<%= pkg.version %>'", + }, + ], + }, + plugin_uri: { + src: ['plugin.php'], + overwrite: true, + replacements: [ + // this is for the plugin_uri comment + { + from: /^( \* Plugin URI:) (.*)/m, + to: '$1 <%= pkg.repository %>/<%= pkg.name %>', + }, + // this is for the github_plugin_uri comment + { + from: /^( \* GitHub Plugin URI:) (.*)/m, + to: '$1 <%= pkg.repository %>/<%= pkg.name %>', + }, + ], + }, + description_readme_txt: { + src: ['readme.txt'], + overwrite: true, + replacements: [ + { + // for this regex to work the readme.txt file MUST have + // unix line endings (Windows won't work). + // note the look ahead. Also, the repeat on the + // newline char class MUST be {2,2}, using just {2} always + // fails + from: /.*(?=[\n\r]{2,2}== Description ==)/m, + to: '<%= pkg.description %>', + }, + ], + }, + description_plugin: { + src: ['plugin.php'], + overwrite: true, + replacements: [ + { + from: /^( \* Description:) (.*)/m, + to: '$1 <%= pkg.description %>', + }, + ], + }, + plugin_name_readme_txt: { + src: ['readme.txt'], + overwrite: true, + replacements: [ + { + from: /^=== (.*) ===/m, + to: '=== <%= pkg.plugin_name %> ===' + }, + ], + }, + plugin_name_plugin: { + src: ['plugin.php'], + overwrite: true, + replacements: [ + { + from: /^( \* Plugin Name:) (.*)/m, + to: '$1 <%= pkg.plugin_name %>', + }, + ], + }, + plugin_name_phpunit: { + src: ['phpunit.xml.dist'], + overwrite: true, + replacements: [ + { + from: /^(\s*)/m, + to: '$1<%= pkg.name %>$3', + }, + ], + }, + tested_up_to: { + src: ['readme.txt'], + overwrite: true, + replacements: [ + { + from: /^(Tested up to:) (.*)/m, + to: '$1 <%= pkg.tested_up_to %>', + }, + ], + }, + license_readme: { + src: ['readme.txt'], + overwrite: true, + replacements: [ + { + from: /^(License:) (.*)/m, + to: '$1 <%= pkg.license %>', + }, + ], + }, + license_uri_readme: { + src: ['readme.txt'], + overwrite: true, + replacements: [ + { + from: /^(License URI:) (.*)/m, + to: '$1 <%= pkg.license_uri %>', + }, + ], + }, + license_uri_plugin: { + src: ['plugin.php'], + overwrite: true, + replacements: [ + { + from: /^( \* License URI:) (.*)/m, + to: '$1 <%= pkg.license_uri %>', + }, + ], + }, + text_domain: { + src: ['plugin.php'], + overwrite: true, + replacements: [ + // this is for the text domain comment + { + from: /^( \* Text Domain:) (.*)/m, + to: '$1 <%= pkg.name %>', + }, + // this is for __() and cousins + { + from: /<%= TextDomain %>/g, + to: '<%= pkg.name %>', + }, + ], + }, + composer_name: { + src: ['composer.json'], + overwrite: true, + replacements: [ + // this is for "name" : "plugin-name" + { + from: /^(\s*"name"\s*:\s*")(.*)"/m, + to: '$1<%= pkg.name %>"', + }, + ], + }, + }, + + // lint JS files + jshint: { + gruntfile: { + options: { + jshintrc: '.jshintrc' + }, + src: 'Gruntfile.js' + }, + assets: { + options: { + jshintrc: '.jshintrc' + }, + // note: we do NOT jshint any of the blocks JS, src or dist. + src: [ + 'assets/**/*.js', '!assets/**/*.min.js', + '!assets/js/blocks/**/*.js' + ] + }, + tests: { + options: { + jshintrc: '.jshintrc' + }, + src: [ + 'tests/**/*.js', + '!tests/**/*.min.js' + ] + }, + }, + + // watch for mods to certain files and fire appropriate tasks + // when they change + // note: I do NOT use this vary often + watch: { + css: { + files: [ + 'assets/css/**/*.scss' + ], + tasks: ['sass'], + options: { + spawn: false, + }, + }, + js: { + files: [ + 'assets/js/**/*.js', + '!assets/js/**/*.min.js' + ], + tasks: ['jshint'], + options: { + spawn: false, + }, + }, + }, + + // Create README.md for GitHub. + wp_readme_to_markdown: { + options: { + screenshot_url: 'assets/images/{screenshot}.png?raw=true' + }, + dest: { + files: { + 'README.md': 'readme.txt' + } + }, +// post_convert: function( content ) { +// content = content.replace( /^### [0-9]+..*$/g, '' ); +// +// return content; +// } + }, + + } ); + + grunt.loadNpmTasks( 'grunt-contrib-clean' ); + grunt.loadNpmTasks( 'grunt-composer' ); + grunt.loadNpmTasks( 'grunt-contrib-uglify-es' ); + grunt.loadNpmTasks( 'grunt-contrib-cssmin' ); + grunt.loadNpmTasks( 'grunt-contrib-jshint' ); + grunt.loadNpmTasks( 'grunt-contrib-copy' ); + grunt.loadNpmTasks( 'grunt-contrib-sass' ); + grunt.loadNpmTasks( 'grunt-contrib-watch' ); + grunt.loadNpmTasks( 'grunt-text-replace' ); + grunt.loadNpmTasks( 'grunt-rtlcss' ); + grunt.loadNpmTasks( 'grunt-wp-readme-to-markdown' ); + grunt.loadNpmTasks( 'grunt-zip' ); + + grunt.registerTask( 'default', ['build'] ); + grunt.registerTask( 'build', [ + 'clean', 'composer:dump-autoload:optimize', //'replace', + 'copy:node_modules', 'sass', 'rtlcss', 'cssmin', 'uglify' + ] ); + grunt.registerTask( 'autoload', ['composer:dump-autoload:optimize'] ); + + grunt.registerTask( 'namespace', ['replace:namespace'] ); + grunt.registerTask( 'autoload', ['composer:dump-autoload:optimize'] ); + grunt.registerTask( 'update', ['composer:update', 'namespace', 'autoload'] ); + grunt.registerTask( 'init', ['replace', 'update'] ); + + // @todo install/configure grunt-phpunit and add it to prerelease + // initial attempts to do so weren't successful. + grunt.registerTask( 'prerelease', ['jshint'] ); + grunt.registerTask( 'readme', [ + 'replace:version_readme_txt', 'replace:plugin_name_readme_txt', + 'replace:description_readme_txt', + 'replace:license_readme', 'replace:license_uri_readme', + 'replace:tested_up_to', + 'wp_readme_to_markdown' + ] ); + grunt.registerTask( 'release', [ + 'build', 'prerelease', 'replace', 'wp_readme_to_markdown', 'copy', 'zip:build', 'clean:release' + ] ); +}; diff --git a/README.md b/README.md new file mode 100644 index 0000000..75feac6 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# REST API Inspector # + +**Contributors:** [pbiron](https://profiles.wordpress.org/pbiron) +**Tags:** REST API, list table +**Requires at least:** 4.6 +**Tested up to:** 5.3.2 +**Stable tag:** 0.1.0 +**License:** GPL-2.0-or-later +**License URI:** https://www.gnu.org/licenses/gpl-2.0.html + +Inspect the REST API routes, endpoints, parameters and properties registered on a site + +## Description ## + +Adds a `Tools > REST API Inspector` menu that presents information about registered REST API routes, endpoints, etc in various list tables. + +### On Screen Help ### + +There is skeletal help in the WP `Help` tabs on each screen, so I won't bother to say much here about how to get started and use this plugin: Just go to `Tools > REST API Inspector` and see the "Help" tab. + +Note: although it may look like there is only one screen (on that Tools menu), each "type" of data (e.g., routes, endpoints, parameters, properties) is displayed in a list table specific to that data "type"...with their own WP Screen instance. So, be sure to check the `Help` tabs on each such screen (especially the `Questions` tabs). + +Some of the on screen help may be incorrect...as I've added new functionality I haven't always had time to update the help tabs (e.g., "Available Actions" tab on the Routes screen doesn't mention the `Schema` and `Handbook` row actions). But, hey, this is only version 0.1.0 of the plugin, so you'll just have to figure some things out for yourselves :-) + +### Implementation ### + +The implementation is *very* preliminary...but basically: + +* there is a `WP_REST_API_List_Table` (in the plugin's PHP namespace, but named so that in the unlikely event folks want this in core, things will be easier :-) + * and various sub-classes of that (e.g., `WP_REST_Routes_List_Table`, `WP_REST_Endpoints_List_Table`, etc) + * these are in `includes/wp-admin/includes` (again, the directory names were chosen to make moving into core easier if that should ever happen) + * there are also various `rest-api-routes-inspector.php`, `rest-api-endpoints-inspector.php`, etc in `includes\wp-admin` that serve the same purpose as core's `wp-admin/edit.php`, `wp-admin/users.php`, etc. + +* there is a rudimentary `WP_REST_API_Query` class that is loosely based on core's `WP_User_Query` + * this class is used by `WP_REST_API_List_Table::prepare_items()`, just like the analogous core query classes are used by the various core list tables + * this class is **GROSSLY** inefficient at this point. I'll work on that in later revisions + +I'm 100% sure there are bugs...but hopefully not major ones at this point. + +I'm also about 90% sure that: + +* I misunderstand some things about the route data returned by [WP_REST_Server::get_routes()](https://developer.wordpress.org/reference/classes/wp_rest_server/get_routes/) (which is where the info that is displayed on all of the screens comes from) +* therefore, some of what is displayed is either `wrong`, `misleading`, `not useful`, etc + +If you want to browse the code, most of it is fairly well documented, with ample DocBlocks (some of which have correct information :-). + +## How Can I Help? ## + +1. Just try the plugin out! See if you can find any bugs. +2. Let me know if you have any suggestions for changes in functionality (either additions or changes to current functionality). +3. See the various `Questions` help tabs, and provide feedback on those questions. + +And finally, answer the big question: Is this plugin something that is worth continuing to work on? That is, does it provide useful information for users/developers (or could it in the future with changes)? + +The best way to provide feedback is to open an [Issue](https://github.com/pbiron/rest-api-inspector). + +## Installation ## + +From your WordPress dashboard + +1. Go to _Plugins > Add New_ and click on _Upload Plugin_ +2. Upload the zip file +3. Activate the plugin + +Also, if you have the awesome [GitHub Updater](https://github.com/afragen/github-updater/) plugin activated, you'll be able to update the plugin when new versions are released, right in the WP Dashboard (or via WP-CLI). + +### Build from sources ### + +1. clone the global repo to your local machine +2. install node.js and npm ([instructions](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)) +3. install composer ([instructions](https://getcomposer.org/download/)) +4. run `npm update` +5. run `composer install` +6. run `grunt build` + * to build a new release zip + * run `grunt release` + +**Important Note:** The build process is a kind of brittle at this point. Why? Because the main composer dependency (a set of "framework" classes I use for writing pllugins) is in a private GitHub repo. So, best *not* to do the `composer install` (or `composer update`) step above :-) + +## Changelog ## + +### 0.1.0 ### + +* init commit diff --git a/assets/css/list-table.css b/assets/css/list-table.css new file mode 100644 index 0000000..bc854ba --- /dev/null +++ b/assets/css/list-table.css @@ -0,0 +1,10 @@ +/* try to give routes with long regexes enough room so they don't wrap */ +table.wp-list-table.routes th#name, +table.wp-list-table.endpoints th#name { + width: 45%; +} + +table.wp-list-table.routes th#methods, +table.wp-list-table.endpoints th#methods { + width: 7%; +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0d5f442 --- /dev/null +++ b/composer.json @@ -0,0 +1,24 @@ +{ + "name" : "wp-rest-api-inspector", + "type" : "wordpress-plugin", + "license" : "GPL-2.0-or-later", + "minimum-stability": "dev", + "config": { + "preferred-install": { + "shc/*": "dist" + } + }, + "repositories": { + "shc-framework": { + "type": "vcs", + "url": "git@github.com:pbiron/shc-framework.git" + } + }, + "autoload" : { + "classmap" : ["plugin.php", "includes", "vendor/shc"], + "exclude-from-classmap": ["components/"] + }, + "require": { + "shc/shc-framework": "^0.1.5" + } +} diff --git a/includes/admin/includes/class-wp-rest-api-list-table.php b/includes/admin/includes/class-wp-rest-api-list-table.php new file mode 100644 index 0000000..dc9494f --- /dev/null +++ b/includes/admin/includes/class-wp-rest-api-list-table.php @@ -0,0 +1,1117 @@ +get_items_per_page( "{$this->screen->rest_item_type}_per_page" ); + $paged = $this->get_pagenum(); + + $args = array( + 'item_type' => $this->screen->rest_item_type, + 'number' => $per_page, + 'offset' => ( $paged - 1 ) * $per_page, + 'search' => ! empty( $_REQUEST['s'] ) ? wp_unslash( trim( $_REQUEST['s'] ) ) : '', + ); + + if ( ! empty( $_REQUEST['route'] ) ) { + $args['route'] = wp_unslash( $_REQUEST['route'] ); + } + + if ( ! empty( $_REQUEST['schema'] ) ) { + $args['route'] = wp_unslash( $_REQUEST['schema'] ); + $args['item_type'] = 'schema'; + } + + if ( ! empty( $_REQUEST['schema-properties'] ) ) { + $args['route'] = wp_unslash( $_REQUEST['schema-properties'] ); + $args['item_type'] = 'schema_property'; + } + + if ( ! empty( $_REQUEST['schema-links'] ) ) { + $args['route'] = wp_unslash( $_REQUEST['schema-links'] ); + $args['item_type'] = 'schema_link'; + } + + if ( ! empty( $_REQUEST['namespace'] ) ) { + $args['namespace'] = wp_unslash( $_REQUEST['namespace'] ); + } + + if ( ! empty( $_REQUEST['method'] ) ) { + $args['method'] = wp_unslash( $_REQUEST['method'] ); + } + + if ( ! empty( $_REQUEST['endpoint'] ) ) { + $args['endpoint'] = wp_unslash( $_REQUEST['endpoint'] ); + } + + if ( ! empty( $_REQUEST['item_status'] ) ) { + $args['item_status'] = wp_unslash( $_REQUEST['item_status'] ); + } + + if ( ! empty( $_REQUEST['type'] ) ) { + $args['type'] = wp_unslash( $_REQUEST['type'] ); + } + + if ( ! empty( $_REQUEST['parameter'] ) ) { + $args['parameter'] = wp_unslash( $_REQUEST['parameter'] ); + } + + if ( ! empty( $_REQUEST['context'] ) ) { + $args['context'] = wp_unslash( $_REQUEST['context'] ); + } + + if ( ! empty( $_REQUEST['array_items_type'] ) ) { + $args['array_items_type'] = wp_unslash( $_REQUEST['array_items_type'] ); + } + + if ( ! empty( $_REQUEST['link_type'] ) ) { + $args['link_type'] = wp_unslash( $_REQUEST['link_type'] ); + } + + if ( ! empty( $_REQUEST['format'] ) ) { + $args['format'] = wp_unslash( $_REQUEST['format'] ); + } + + if ( ! empty( $_REQUEST['orderby'] ) ) { + $args['orderby'] = $_REQUEST['orderby']; + } + + if ( ! empty( $_REQUEST['order'] ) ) { + $args['order'] = $_REQUEST['order']; + } + + /** + * Filters the query arguments used to retrieve REST API items for the current list table. + * + * @since 0.1.0 + * + * @param array $args Arguments passed to WP_Rest_API_Query to retrieve items for the current + * list table. + */ + $args = apply_filters( 'rest_api_list_table_query_args', $args ); + + // Query the user IDs for this page + $rest_api_search = new WP_REST_API_Query( $args ); + + $this->items = $rest_api_search->get_results(); + $this->all_items = $rest_api_search->get_all_results(); + + $this->set_pagination_args( + array( + 'total_items' => $rest_api_search->get_total(), + 'per_page' => $per_page, + ) + ); + + return; + } + + /** + * @param object[] $items + * @param int $level + * @return void + */ + public function display_rows1( $items = array(), $level = 0 ) { + if ( $this->hierarchical_display ) { + if ( empty ( $items ) ) { + $items = $this->all_items; + } + $this->_display_rows_hierarchical( $items, $this->get_pagenum(), 20 ); + } + else { + if ( empty ( $items ) ) { + $items = $this->items; + } + foreach ( $items as $item ) { + $this->single_row( $item, $level ); + } + } + + return; + } + + /** + * @param array $items + * @param int $pagenum + * @param int $per_page + */ + protected function _display_rows_hierarchical( $items, $pagenum = 1 ) { + $level = 0; + + if ( ! $items ) { + return; + } + + /* + * Arrange pages into two parts: top level pages and children_pages + * children_pages is two dimensional array, eg. + * children_pages[10][] contains all sub-pages whose parent is 10. + * It only takes O( N ) to arrange this and it takes O( 1 ) for subsequent lookup operations + * If searching, ignore hierarchy and treat everything as top level + */ + if ( empty( $_REQUEST['s'] ) ) { + $top_level_items = array(); + $children_items = array(); + + foreach ( $items as $item ) { + if ( '' === $item->parent ) { + $top_level_items[] = $item; + } + else { + $children_items[ $item->parent ][] = $item; + } + } + + $items = &$top_level_items; + } + + $per_page = $this->get_pagination_arg( 'per_page' ); + $count = 0; + $start = ( $pagenum - 1 ) * $per_page; + $end = $start + $per_page; + $to_display = array(); + + foreach ( $items as $item ) { + if ( $count >= $end ) { + break; + } + + if ( $count >= $start ) { + $to_display[ $item->name ] = $level; + } + + $count++; + + if ( isset( $children_items ) ) { + $this->_item_rows( $children_items, $count, $item->name, $level + 1, $pagenum, $per_page, $to_display ); + } + } + + // If it is the last pagenum and there are orphaned pages, display them with paging as well. + if ( isset( $children_items ) && $count < $end ) { + foreach ( $children_items as $orphans ) { + foreach ( $orphans as $op ) { + if ( $count >= $end ) { + break; + } + + if ( $count >= $start ) { + $to_display[ $op->name ] = 0; + } + + $count++; + } + } + } + + foreach ( $to_display as $name => $level ) { + echo "\t"; + $this->single_row( $this->all_items[ $name ], $level ); + } + + return; + } + + /** + * @global WP_Post $post Global post object. + * + * @param int|WP_Post $item + * @param int $level + */ + public function single_row( $item, $level = 0 ) { + $this->current_level = $level; + + $classes = ''; + if ( isset( $item->parent ) ) { + $count = count( get_post_ancestors( $item->name ) ); + $classes .= ' level-' . $count; + } else { + $classes .= ' level-0'; + } + ?> + + single_row_columns( $item ); ?> + + = $end ) { + break; + } + + // If the page starts in a subtree, print the parents. + if ( $count == $start && '' !== $item->parent ) { + $my_parents = array(); + $my_parent = $item->parent; + while ( $my_parent ) { + // Get the ID from the list or the attribute if my_parent is an object + $parent_id = $my_parent; + if ( is_object( $my_parent ) ) { + $parent_id = $my_parent; + } + + $my_parent = $this->get_item( $parent_id ); + $my_parents[] = $my_parent; + if ( ! $my_parent ) { + break; + } + $my_parent = $my_parent->parent; + } + $num_parents = count( $my_parents ); + while ( $my_parent = array_pop( $my_parents ) ) { + $to_display[ $my_parent->name ] = $level - $num_parents; + $num_parents--; + } + } + + if ( $count >= $start ) { + $to_display[ $item->name ] = $level; + } + + $count++; + + $this->_item_rows( $children_items, $count, $item->name, $level + 1, $pagenum, $per_page, $to_display ); + } + + unset( $children_items[ $parent ] ); //required in order to keep track of orphans + + return; + } + + protected function get_item( $name ) { + return $this->all_items[ $name ]; + } + + /** + * Retrieves the view "next" link for an item. + * + * What the "next" is depends on the item type for the given item. + * If the given item is a route, then the "next" is the endpoints for + * that route; if the given is an endpoint, then the "next" is the + * parameters for that endpoint, etc. + * + * This method is similar to core's + * {@link https://developer.wordpress.org/reference/functions/get_edit_post_link/ get_edit_post_link()}. + * However, the analogy is pretty tenuous, so maybe it should be renamed to + * avoid confusion. + * + * @since 0.1.0 + * + * @param object $item The current item. + * @return string The view "next" URL for the given item. + */ + protected function get_view_next_link( $item ) { + die( 'function WP_REST_API_List_Table::get_view_next_link() must be over-ridden in a sub-class.' ); + } + + /** + * Return an associative array listing all the views that can be used + * with this table. + * + * @since 0.1.0 + * + * @return array An array of HTML links, one for each view. + */ + protected function get_views() { + $total_items = count( $this->all_items ); + $all_views = $this->_all_views(); + $avail_views = $this->_avail_views(); + + $view = ! empty( $_REQUEST['item_status'] ) ? $_REQUEST['item_status'] : ''; + $current_link_attributes = empty( $view ) ? ' class="current" aria-current="page"' : ''; + + $url = add_query_arg( array( 'page' => $_REQUEST['page'] ), admin_url( 'tools.php' ) ); + if ( ! empty( $_REQUEST['route'] ) ) { + $url = add_query_arg( 'route', urlencode( wp_unslash( $_REQUEST['route'] ) ), $url ); + } + if ( ! empty( $_REQUEST['endpoint'] ) ) { + $url = add_query_arg( 'endpoint', $_REQUEST['endpoint'], $url ); + } + if ( ! empty( $_REQUEST['methods'] ) ) { + $url = add_query_arg( 'methods', $_REQUEST['methods'], $url ); + } + if ( ! empty( $_REQUEST['parameter'] ) ) { + $url = add_query_arg( 'parameter', $_REQUEST['parameter'], $url ); + } + if ( ! empty( $_REQUEST['schema-properties'] ) ) { + $url = add_query_arg( 'schema-properties', urlencode( wp_unslash( $_REQUEST['schema-properties'] ) ), $url ); + } + + $view_links = array(); + $view_links['all'] = sprintf( + '%s', + $url, + $current_link_attributes, + sprintf( + /* translators: %s: Number of users. */ + _nx( + 'All (%s)', + 'All (%s)', + $total_items, + 'rest_items' + ), + number_format_i18n( $total_items ) + ) + ); + + foreach ( $all_views as $this_view => $name ) { + if ( ! isset( $avail_views[ $this_view ] ) || 0 === $avail_views[ $this_view ] ) { + continue; + } + + $current_link_attributes = ''; + + if ( $this_view === $view ) { + $current_link_attributes = ' class="current" aria-current="page"'; + } + + $name = sprintf( + /* translators: User role name with count. */ + __( '%1$s (%2$s)' ), + $name, + number_format_i18n( $avail_views[ $this_view ] ) + ); + + $view_links[ $this_view ] = sprintf( + '%s', + esc_url( add_query_arg( 'item_status', $this_view, $url ) ), + $current_link_attributes, + $name + ); + } + + return $view_links; + } + + /** + * Display a methods dropdown for filtering items. + * + * @since 0.1.0 + * + * @eturn void + */ + protected function methods_dropdown() { + $this->dropdown( + 'method', + __( 'Filter by method', 'shc-rest-api-inspector' ), + __( 'All methods', 'shc-rest-api-inspector' ), + array( + 'GET' => 'GET', + 'POST' => 'POST', + 'PUT' => 'PUT', + 'PATCH' => 'PATCH', + 'DELETE' => 'DELETE', + ) + ); + + return; + } + + /** + * Display a namespaces dropdown for filtering items. + * + * @since 0.1.0 + * + * @eturn void + */ + protected function namespaces_dropdown() { + $rest_server = rest_get_server(); + $_namespaces = $rest_server->get_namespaces(); + + $options = array(); + foreach ( $_namespaces as $namespace ) { + $options[ $namespace ] = $namespace; + } + + $this->dropdown( + 'namespace', + __( 'Filter by namespace', 'shc-rest-api-inspector' ), + __( 'All namespaces', 'shc-rest-api-inspector' ), + $options + ); + + return; + } + + /** + * Display a types dropdown for filtering items. + * + * @since 0.1.0 + * + * @eturn void + */ + protected function types_dropdown() { + $types = $options = array(); + foreach ( $this->all_items as $item ) { + $types = array_merge( $types, $item->type ); + } + + if ( empty( $types ) ) { + // essentially, the same as the 'hide_if_empty' arg on core's category_dropdown(). +// return; + } + + foreach ( array_unique( $types ) as $type ) { + $options[ $type ] = $type; + } + + $this->dropdown( + 'type', + __( 'Filter by type', 'shc-rest-api-inspector' ), + __( 'All types', 'shc-rest-api-inspector' ), + $options + ); + + return; + } + + /** + * Get a list of columns. The format is: + * 'internal-name' => 'Title' + * + * @since 0.1.0 + * + * @return array + */ + function get_columns() { + $columns = array(); + + // we don't have any bulk actions by default. If someone has added them + // by hooking into bulk_actions-{$this->screen->id}, then make sure we + // have a checkbox column. + if ( has_filter( "bulk_actions-{$this->screen->id}" ) ) { + $columns = array( 'cb' => '' ) + $columns; + } + + return $columns; + } + + /** + * Get a list of sortable columns. The format is: + * 'internal-name' => 'orderby' + * or + * 'internal-name' => array( 'orderby', true ) + * + * The second format will make the initial sorting order be descending + * + * @since 0.1.0 + * + * @return array + */ + protected function get_sortable_columns() { + return array( + 'name' => 'name', + ); + } + + /** + * Gets the name of the default primary column. + * + * @since 0.1.0 + * + * @return string Name of the default primary column, in this case, 'name'. + */ + protected function get_default_primary_column_name() { + return 'name'; + } + + /** + * Extra controls to be displayed between bulk actions and pagination. + * + * @since 0.1.0 + * + * @param string $which + */ + protected function extra_tablenav( $which ) { + ?> +
+do_dropdowns(); + + /** + * Fires before the Filter button on the Posts and Pages list tables. + * + * The Filter button allows sorting by date and/or category on the + * Posts list table, and sorting by date on the Pages list table. + * + * @since 0.1.0 + * + * @param string $rest_item_type The rest item type slug. + * @param string $which The location of the extra table nav markup: + * 'top' or 'bottom'. + */ + do_action( 'restrict_manage_rest_items', $this->screen->rest_item_type, $which ); + + $output = ob_get_clean(); + + if ( ! empty( $output ) ) { + echo $output; + submit_button( __( 'Filter' ), '', 'filter_action', false, array( 'id' => 'rest-item-query-submit' ) ); + } + } + ?> +
+{$column_name} ) ) { + return implode( '
', array_map( 'esc_html', $item->{$column_name} ) ); + } + elseif ( is_bool( $item->{$column_name} ) ) { + return esc_html( + $item->{$column_name} ? + __( 'true', 'shc-rest-api-inspector' ) : + __( 'false', 'shc-rest-api-inspector' ) ); + } + else { + return esc_html( $item->{$column_name} ); + } + } + + /** + * Handles the checkbox column output. + * + * @since 0.1.0 + * + * @param object $item The current item object. + */ + function column_cb( $item ) { + ?> + + +has_view_next( $item ) ) { + $actions['view'] = sprintf( + '%s', + $this->get_view_next_link( $item ), + /* translators: %s: Post title. */ + esc_attr( sprintf( __( 'View “%s”' ), $item->name ) ), + $this->get_view_text()//__( 'View' ) + ); + } + + $actions = array_merge( $actions, $this->_get_row_actions( $item ) ); + + /** + * Filters the array of row action links on the Pages list table. + * + * The filter is evaluated only for hierarchical post types. + * + * @since 0.1.0 + * + * @param string[] $actions An array of row action links. Defaults are 'View'. + * @param object $item The rest api item object. + */ + $actions = apply_filters( "rest_{$this->screen->rest_item_type}_row_actions", $actions, $item ); + + /** + * Filters the array of row action links on the Pages list table. + * + * The filter is evaluated only for hierarchical post types. + * + * @since 0.1.0 + * + * @param string[] $actions An array of row action links. Defaults are 'View'. + * @param object $item The rest api item object. + */ + $actions = apply_filters( 'rest_item_row_actions', $actions, $item ); + + return $this->row_actions( $actions ); + } + + protected function _get_row_actions( $item ) { + $actions = array(); + + if ( $this->has_view_next( $item ) ) { + $actions['view'] = sprintf( + '%s', + $this->get_view_next_link( $item ), + /* translators: %s: Post title. */ + esc_attr( sprintf( __( 'View “%s”' ), $item->name ) ), + $this->get_view_text()//__( 'View' ) + ); + } + + return $actions; + } + + /** + * Handles the title column output. + * + * @since 0.1.0 + * + * @param object $item The current item object. + */ + function column_name( $item ) { + if ( $this->hierarchical_display ) { + if ( 0 === $this->current_level && '' !== $item->parent ) { + // Sent level 0 by accident, by default, or because we don't know the actual level. + $find_main_page = $item->parent; + while ( '' !== $find_main_page ) { + $parent = $this->get_item( $find_main_page ); + + if ( is_null( $parent ) ) { + break; + } + + $this->current_level++; + $find_main_page = $parent->parent; + + if ( ! isset( $parent_name ) ) { + $parent_name = $parent->name; + } + } + } + } + + $pad = str_repeat( '— ', $this->current_level ); + echo ''; + + if ( $this->has_view_next( $item ) ) { + printf( + '%s%s', + $this->get_view_next_link( $item ), + /* translators: %s: Post title. */ + esc_attr( sprintf( __( '“%s” (View)' ), $item->name ) ), + $pad, + esc_html( $item->name ) + ); + } + else { + printf( + '%s%s', + $pad, + esc_html( $item->name ) + ); + } + + $this->_item_states( $item ); + + if ( isset( $parent_name ) ) { + echo ' | ' . ':' . ' ' . esc_html( $parent_name ); + } + + echo ''; + + return; + } + + /** + * Handles the methods column output. + * + * @since 0.1.0 + * + * @param object $item The current item object. + */ + function column_methods( $item ) { + $methods = $_methods = array(); + + foreach ( $item->methods as $method ) { + $method = explode( ' | ', $method ); + $_methods = array_merge( $_methods, $method ); + } + foreach ( $_methods as $method ) { + $url = add_query_arg( 'page', $_REQUEST['page'], admin_url( 'tools.php' ) ); + if ( ! empty( $_REQUEST['route'] ) ) { + $url = add_query_arg( 'route', urlencode( wp_unslash( $_REQUEST['route'] ) ), $url ); + } + $methods[] = sprintf( + '%s', + add_query_arg( 'method', $method, $url ), + $method + ); + } + + return implode( '
', $methods ); + } + + /** + * Handles the type column output. + * + * @since 0.1.0 + * + * @param object $item The current item object. + */ + function column_type( $item ) { + $types = array(); + + foreach ( $item->type as $type ) { + $url = add_query_arg( 'page', $_REQUEST['page'], admin_url( 'tools.php' ) ); + if ( ! empty( $_REQUEST['route'] ) ) { + $url = add_query_arg( 'route', urlencode( $_REQUEST['route'] ), $url ); + } + if ( ! empty( $_REQUEST['endpoint'] ) ) { + $url = add_query_arg( 'endpoint', $_REQUEST['endpoint'], $url ); + } + if ( ! empty( $_REQUEST['methods'] ) ) { + $url = add_query_arg( 'methods', $_REQUEST['methods'], $url ); + } + if ( ! empty( $_REQUEST['parameter'] ) ) { + $url = add_query_arg( 'parameter', $_REQUEST['parameter'], $url ); + } + if ( ! empty( $_REQUEST['schema-properties'] ) ) { + $url = add_query_arg( 'schema-properties', urlencode( wp_unslash( $_REQUEST['schema-properties'] ) ), $url ); + } + $types[] = sprintf( + '%s', + add_query_arg( 'type', $type, $url ), + $type + ); + } + + return implode( '
', $types ); + } + + /** + * Handles the context column output. + * + * @since 0.1.0 + * + * @param object $item The current route object. + */ + function column_context( $item ) { + $contexts = array(); + + foreach ( $item->context as $context ) { + $url = add_query_arg( 'page', $_REQUEST['page'], admin_url( 'tools.php' ) ); + if ( ! empty( $_REQUEST['route'] ) ) { + $url = add_query_arg( 'route', urlencode( $_REQUEST['route'] ), $url ); + } + if ( ! empty( $_REQUEST['methods'] ) ) { + $url = add_query_arg( 'methods', $_REQUEST['methods'], $url ); + } + if ( ! empty( $_REQUEST['parameter'] ) ) { + $url = add_query_arg( 'parameter', $_REQUEST['parameter'], $url ); + } + if ( ! empty( $_REQUEST['schema-properties'] ) ) { + $url = add_query_arg( 'schema-properties', urlencode( wp_unslash( $_REQUEST['schema-properties'] ) ), $url ); + } + $contexts[] = sprintf( + '%s', + add_query_arg( 'context', $context, $url ), + $context + ); + } + + return implode( '
', $contexts ); + } + + /** + * Echo or return the post states as HTML. + * + * The name of this method comes from core's + * {@link https://developer.wordpress.org/reference/functions/_post_states _post_states(). + * + * @since 0.1.0 + * + * @see WP_REST_API_List_Table::get_item_states() + * + * @param object $item The REST API item to retrieve states for. + * @param bool $echo Optional. Whether to echo the post states as an HTML string. Default true. + * @return string Item states string. + */ + function _item_states( $item, $echo = true ) { + $item_states = $this->get_item_states( $item ); + $item_states_string = ''; + + if ( ! empty( $item_states ) ) { + $state_count = count( $item_states ); + $i = 0; + + $item_states_string .= ' — '; + foreach ( $item_states as $state ) { + ++$i; + ( $i == $state_count ) ? $sep = '' : $sep = ', '; + $item_states_string .= "$state$sep"; + } + } + + if ( $echo ) { + echo $item_states_string; + } + + return $item_states_string; + } + + /** + * Retrieve an array of item states from a REST API item. + * + * The name of this method comes from core's + * {@link https://developer.wordpress.org/reference/functions/_post_states/ get_post_states()}. + * + * @since 0.1.0 + * + * @param object $item The item to retrieve states for. + * @return array The array of translated item states. + */ + protected function get_item_states( $item ) { + $item_states = array(); + if ( isset( $_REQUEST['item_status'] ) ) { + $item_status = $_REQUEST['item_status']; + } else { + $item_status = ''; + } + + $item_states = $this->_get_item_states( $item, $item_status ); + + /** + * Filters the default item display states used in the REST API list table. + * + * @since 0.1.0 + * + * @param string[] $item_states An array of item display states. + * @param object $item The current REST API item object. + */ + return apply_filters( 'display_rest_item_states', $item_states, $item ); + } + + /** + * Get all views for this list table. + * + * @since 0.1.0 + * + * @return string[] Array of views. Keys are item statuses, values are + * the text to display for the view. + */ + protected function _all_views() { + return array( + 'maybe' => __( 'Maybe Authenticated', 'shc-rest-api-inspector' ), + 'yes' => __( 'Authenticated', 'shc-rest-api-inspector' ), + 'no' => __( 'Unauthenticated', 'shc-rest-api-inspector' ), + ); + } + + /** + * Get all available views. + * + * The return value is analogous to the `$avail_post_stati` global var that is + * set in + * {@link https://developer.wordpress.org/reference/classes/wp_posts_list_table/prepare_items/ WP_Posts_List_Table::prepare_items()}. + * + * @since 0.1.0 + * + * @return array Array of available views. Keys are item status, values are + * counts of items with that item status. + */ + protected function _avail_views() { + $avail_views = array(); + + foreach ( array( 'maybe', 'yes', 'no' ) as $view ) { + $avail_views[ $view ] = count( wp_list_filter( $this->all_items, array( 'authenticated' => $view ) ) ); + } + + return $avail_views; + } + + /** + * Helper for displaying dropdowns for filtering items. + * + * @since 0.1.0 + * + * @param string $name The name for the select element. + * @param string $screen_reader_text The text for the screen reader. + * @param string $all_items_text The text for the "All items" option in the select element. + * @param array $options Associative array of options for the select element. + * Keys are option values and values are option labels. + * The labels should *not* be previously HTML escaped. + * @return void + */ + protected function dropdown( $name, $screen_reader_text, $all_items_text, $options ) { + $displayed = isset( $_REQUEST[ $name ] ) ? $_REQUEST[ $name ] : ''; + ?> + + +authenticated && 'maybe' !== $item_status ) { + $item_states['authenticated'] = __( 'Maybe Authenticated', 'shc-rest-api-inspector' ); + } + if ( 'yes' === $item->authenticated && 'yes' !== $item_status ) { + $item_states['authenticated'] = __( 'Authenticated', 'shc-rest-api-inspector' ); + } + elseif ( 'no' === $item->authenticated && 'no' !== $item_status ) { + $item_states['not_authenticated'] = __( 'Unauthenticated', 'shc-rest-api-inspector' ); + } + + return $item_states; + } + + /** + * Can a given item drill down into the "next" item? + * + * For example, if the item is a route, then it can drill down into it's endpoints; + * if it is an endpoint, it can drill down into it's parameters, etc. + * + * @since 0.1.0 + * + * @param object $item + * @return bool True if the given item can drill down into the "next" item; false otherwise. + */ + protected function has_view_next( $item ) { + return true; + } +} diff --git a/includes/admin/includes/class-wp-rest-endpoints-list-table.php b/includes/admin/includes/class-wp-rest-endpoints-list-table.php new file mode 100644 index 0000000..4b972a5 --- /dev/null +++ b/includes/admin/includes/class-wp-rest-endpoints-list-table.php @@ -0,0 +1,277 @@ + 'endpoint', + 'plural' => 'endpoints', + ); + + $args = wp_parse_args( $args, $defaults ); + + parent::__construct( $args ); + } + + /** + * Get a list of columns. The format is: + * 'internal-name' => 'Title' + * + * @since 0.1.0 + * + * @return array + */ + function get_columns() { + $columns = array( + 'name' => __( 'Route', 'shc-rest-api-inspector' ), + 'methods' => __( 'Methods', 'shc-rest-api-inspector' ), + 'permission_callback' => __( 'Permission Callback', 'shc-rest-api-inspector' ), + 'callback' => __( 'Callback', 'shc-rest-api-inspector' ), + 'show_in_index' => __( 'Show in Index', 'shc-rest-api-inspector' ), + 'accept_json' => __( 'Accept JSON', 'shc-rest-api-inspector' ), + 'accept_raw' => __( 'Accept RAW', 'shc-rest-api-inspector' ), + ); + + return parent::get_columns() + $columns; + } + + /** + * Handles the permission_callback column output. + * + * Produces a link to the WP Code Reference for the callback, which of course + * will be incorrect for non-core endpoints. + * + * @since 0.1.0 + * + * @param object $item The current endpoint object. + * + * @todo figure out a reliable way to distinquish core from non-core endpoints + * and just output the callback "name" for non-core endpoints. + */ + function column_permission_callback( $item ) { + return $this->_get_code_reference_link( $item->permission_callback ); + } + + /** + * Handles the callback column output. + * + * Produces a link to the WP Code Reference for the callback, which of course + * will be incorrect for non-core endpoints. + * + * @since 0.1.0 + * + * @param object $item The current endpoint object. + * + * @todo figure out a reliable way to distinquish core from non-core endpoints + * and just output the callback "name" for non-core endpoints. + */ + function column_callback( $item ) { + return $this->_get_code_reference_link( $item->callback ); + } + + protected function _get_code_reference_link( $callback ) { + $url = $this->_get_code_reference_url( $callback ); + + if ( is_array( $callback ) ) { + if ( is_string( $callback[0] ) ) { + // not a core route. + // nothing to do, so bail. + // @todo this isn't 100% reliable, but catches the case of + // of the endpoint(s) registered by the wp-rest-api-log plugin. + return sprintf( '%s::%s()', $callback[0], $callback[1] ); + } + + $link_text = sprintf( '%s::%s()', get_class( $callback[0] ), $callback[1] ); + } + else { + $link_text = $callback; + } + + if ( $url ) { + return sprintf( + '%2$s %3$s', + esc_url( $url ), + esc_html( $link_text ), + /* translators: Accessibility text. */ + __( '(opens in a new tab)' ) + ); + } + else { + return $link_text; + } + } + + /** + * Get the URL for a callback in the WP Code Reference. + * + * @since 0.1.0 + * + * @param callable $callback The callback to get the Code Reference URL for. + * @return string + * + * @todo add error checking to the Reflection stuff. + * @todo see if there is a way to validate that the URL actually resolves to + * something in the Code Reference without doing a HEAD request...which + * could get expensive (and probably piss-off the systems folks). + */ + protected function _get_code_reference_url( $callback ) { + if ( is_array( $callback ) ) { + if ( ! $this->is_core_class( $callback[0] ) ) { + return ''; + } + + $method = new ReflectionMethod( $callback[0], $callback[1] ); + $declaring_class = $method->getDeclaringClass()->name; + + $url = sprintf( + 'https://developer.wordpress.org/reference/classes/%s/%s/', + strtolower( $declaring_class ), + $callback[1] + ); + } + else { + $url = sprintf( + 'https://developer.wordpress.org/reference/functions/%s/', + $callback + ); + } + + return $url; + } + + /** + * Is a given class one of the core REST API classes (e.g., controllers)? + * + * This is used to know whether to output a link to WP Code Reference entry for callbacks. + * + * @since 0.1.0 + * + * @param object|string $class THe class to check. + * @return bool True if `$class` is a core class, false otherwise. + */ + protected function is_core_class( $class ) { + if ( is_object( $class ) ) { + $class = get_class( $class ); + } + + switch ( $class ) { + // the main rest server. + case 'WP_REST_Server': + // the core controllers. + case 'WP_REST_Attachments_Controller': + case 'WP_REST_Autosaves_Controller': + case 'WP_REST_Block_Renderer_Controller': + case 'WP_REST_Blocks_Controller': + case 'WP_REST_Comments_Controller': + case 'WP_REST_Post_Statuses_Controller': + case 'WP_REST_Post_Types_Controller': + case 'WP_REST_Posts_Controller': + case 'WP_REST_Revisions_Controller': + case 'WP_REST_Search_Controller': + case 'WP_REST_Settings_Controller': + case 'WP_REST_Taxonomies_Controller': + case 'WP_REST_Terms_Controller': + case 'WP_REST_Themes_Controller': + case 'WP_REST_Users_Controller': + return true; + default: + return false; + } + } + + /** + * Can a given item drill down into the "next" item? + * + * Parameters can only drill down further if their type is `object`. + * + * @since 0.1.0 + * + * @param object $item + * @return bool True if the given item can drill down into the "next" item; false otherwise. + */ + protected function has_view_next( $item ) { + return ! empty( $item->args ); + } + + /** + * Gext the text to display for the "View" row action. + * + * @since 0.1.0 + * + * @return string + */ + protected function get_view_text() { + return __( 'Parameters', 'shc-rest-api-inspector' ); + } + + /** + * Retrieves the view parameters link for an endpoint. + * + * This method is similar to core's + * {@link https://developer.wordpress.org/reference/functions/get_edit_post_link/ get_edit_post_link()}. + * However, the analogy is pretty tenuous, so maybe it should be renamed to + * avoid confusion. + * + * @since 0.1.0 + * + * @param object $item The current endpoint item. + * @return string The view parameters URL for the given endpoint. + */ + protected function get_view_next_link( $item ) { + return add_query_arg( + array( + 'page' => $_REQUEST['page'], + 'route' => urlencode( wp_unslash( $_REQUEST['route'] ) ), + 'endpoint' => implode( ' | ', $item->methods ), + ), + admin_url( 'tools.php' ) + ); + } + + /** + * Output dropdowns. + * + * @since 0.1.0 + * + * @return void + */ + protected function do_dropdowns() { + $this->methods_dropdown(); + + return; + } +} diff --git a/includes/admin/includes/class-wp-rest-parameters-list-table.php b/includes/admin/includes/class-wp-rest-parameters-list-table.php new file mode 100644 index 0000000..c4d2978 --- /dev/null +++ b/includes/admin/includes/class-wp-rest-parameters-list-table.php @@ -0,0 +1,252 @@ + 'parameter', + 'plural' => 'parameters', + ); + + $args = wp_parse_args( $args, $defaults ); + + parent::__construct( $args ); + } + + /** + * Get a list of columns. The format is: + * 'internal-name' => 'Title' + * + * @since 0.1.0 + * + * @return array + */ + function get_columns() { + $columns = array( + 'name' => __( 'Name', 'shc-rest-api-inspector' ), + 'type' => __( 'Type', 'shc-rest-api-inspector' ), + 'array_items_type'=> __( 'Array Items Type', 'shc-rest-api-inspector' ), + 'description' => __( 'Description', 'shc-rest-api-inspector' ), + 'default_value' => __( 'Default', 'shc-rest-api-inspector' ), + 'enum' => __( 'Enum', 'shc-rest-api-inspector' ), + ); + + return parent::get_columns() + $columns; + } + + /** + * Handles the array items type column output. + * + * @since 0.1.0 + * + * @param object $item The current item object. + */ + function column_array_items_type( $item ) { + $array_items_types = array(); + + foreach ( $item->array_items_type as $array_items_type ) { + $url = add_query_arg( 'page', $_REQUEST['page'], admin_url( 'tools.php' ) ); + if ( ! empty( $_REQUEST['route'] ) ) { + $url = add_query_arg( 'route', urlencode( $_REQUEST['route'] ), $url ); + } + if ( ! empty( $_REQUEST['endpoint'] ) ) { + $url = add_query_arg( 'endpoint', $_REQUEST['endpoint'], $url ); + } + if ( ! empty( $_REQUEST['methods'] ) ) { + $url = add_query_arg( 'methods', wp_unslash( $_REQUEST['methods'] ), $url ); + } + $array_items_types[] = sprintf( + '%s', + add_query_arg( 'array_items_type', $array_items_type, $url ), + $array_items_type + ); + } + + return implode( '
', $array_items_types ); + } + + /** + * Gext the text to display for the "View" row action. + * + * @since 0.1.0 + * + * @return string + */ + protected function get_view_text() { + return __( 'Properties', 'shc-rest-api-inspector' ); + } + + /** + * Retrieves the view properties link for a parameter. + * + * This method is similar to core's + * {@link https://developer.wordpress.org/reference/functions/get_edit_post_link/ get_edit_post_link()}. + * However, the analogy is pretty tenuous, so maybe it should be renamed to + * avoid confusion. + * + * @since 0.1.0 + * + * @param object $item The current parameter item. + * @return string The view properties URL for the given parameter. + */ + protected function get_view_next_link( $item ) { + return add_query_arg( + array( + 'page' => $_REQUEST['page'], + 'route' => urlencode( wp_unslash( $_REQUEST['route'] ) ), + 'endpoint' => $_REQUEST['endpoint'], + 'parameter' => $item->name, + ), + admin_url( 'tools.php' ) + ); + } + + /** + * Output dropdowns. + * + * @since 0.1.0 + * + * @return void + */ + protected function do_dropdowns() { + $this->types_dropdown(); + $this->array_items_type_dropdown(); + + return; + } + + /** + * Display an array items types dropdown for filtering items. + * + * @since 0.1.0 + * + * @eturn void + */ + protected function array_items_type_dropdown() { + $array_item_types = $options = array(); + foreach ( $this->all_items as $item ) { + $array_item_types = array_merge( $array_item_types, $item->array_items_type ); + } + + if ( empty( $array_item_types ) ) { + // essentially, the same as the 'hide_if_empty' arg on core's category_dropdown(). +// return; + } + + foreach ( array_unique( $array_item_types ) as $array_item_type ) { + $options[ $array_item_type ] = $array_item_type; + } + + $this->dropdown( + 'array_items_type', + __( 'Filter by array item type', 'shc-rest-api-inspector' ), + __( 'All array item types', 'shc-rest-api-inspector' ), + $options + ); + + return; + } + + /** + * Get all views for this list table. + * + * @since 0.1.0 + * + * @return string[] Array of views. Keys are item statuses, values are + * the text to display for the view. + */ + protected function _all_views() { + return array( + 'required' => __( 'Required', 'shc-rest-api-inspector' ), + 'optional' => __( 'Optional', 'shc-rest-api-inspector' ), + ); + } + + /** + * Get all available views. + * + * The return value is analogous to the `$avail_post_stati` global var that is + * set in + * {@link https://developer.wordpress.org/reference/classes/wp_posts_list_table/prepare_items/ WP_Posts_List_Table::prepare_items()}. + * + * @since 0.1.0 + * + * @return array Array of available views. Keys are item status, values are + * counts of items with that item status. + */ + protected function _avail_views() { + $avail_views = array(); + + foreach ( array( 'required' => true, 'optional' => false ) as $view => $value ) { + $avail_views[ $view ] = count( wp_list_filter( $this->all_items, array( 'required' => $value ) ) ); + } + + return $avail_views; + } + + /** + * Retrieve an array of item states for an item. + * + * @since 0.1.0 + * + * @see WP_REST_API_List_Table::get_item_states() + * + * @param object $item The item to retrieve states for. + * @return array Array of item states. + */ + protected function _get_item_states( $item, $item_status ) { + $item_states = array(); + + if ( true === $item->required && 'required' !== $item_status ) { + $item_states['required'] = __( 'Required', 'shc-rest-api-inspector' ); + } + + return $item_states; + } + + /** + * Can a given item drill down into the "next" item? + * + * Parameters can only drill down further if their type is `object`. + * + * @since 0.1.0 + * + * @param object $item + * @return bool True if the given item can drill down into the "next" item; false otherwise. + */ + protected function has_view_next( $item ) { + return ! empty( $item->properties ); + } +} diff --git a/includes/admin/includes/class-wp-rest-properties-list-table.php b/includes/admin/includes/class-wp-rest-properties-list-table.php new file mode 100644 index 0000000..de59729 --- /dev/null +++ b/includes/admin/includes/class-wp-rest-properties-list-table.php @@ -0,0 +1,181 @@ + 'property', + 'plural' => 'properties', + ); + + $args = wp_parse_args( $args, $defaults ); + + parent::__construct( $args ); + } + + /** + * Get a list of columns. The format is: + * 'internal-name' => 'Title' + * + * @since 0.1.0 + * + * @return array + */ + function get_columns() { + $columns = array( + 'name' => __( 'Name', 'shc-rest-api-inspector' ), + 'type' => __( 'Type', 'shc-rest-api-inspector' ), + 'description' => __( 'Description', 'shc-rest-api-inspector' ), + 'context' => __( 'Context', 'shc-rest-api-inspector' ), + ); + + return parent::get_columns() + $columns; + } + + /** + * Output dropdowns. + * + * @since 0.1.0 + * + * @return void + */ + protected function do_dropdowns() { + $this->types_dropdown(); + $this->contexts_dropdown(); + + return; + } + + /** + * Display a contexts dropdown for filtering items. + * + * @since 0.1.0 + * + * @eturn void + */ + protected function contexts_dropdown() { + $contexts = $options = array(); + foreach ( $this->all_items as $item ) { + $contexts = array_merge( $contexts, $item->context ); + } + + foreach ( array_unique( $contexts ) as $context ) { + $options[ $context ] = $context; + } + + $this->dropdown( + 'context', + __( 'Filter by context', 'shc-rest-api-inspector' ), + __( 'All contexts', 'shc-rest-api-inspector' ), + $options + ); + + return; + } + + /** + * Get all views for this list table. + * + * @since 0.1.0 + * + * @return string[] Array of views. Keys are item statuses, values are + * the text to display for the view. + */ + protected function _all_views() { + return array( + 'readonly' => __( 'Readonly', 'shc-rest-api-inspector' ), + 'writeable' => __( 'Writable', 'shc-rest-api-inspector' ), + ); + } + + /** + * Get all available views. + * + * The return value is analogous to the `$avail_post_stati` global var that is + * set in + * {@link https://developer.wordpress.org/reference/classes/wp_posts_list_table/prepare_items/ WP_Posts_List_Table::prepare_items()}. + * + * @since 0.1.0 + * + * @return array Array of available views. Keys are item status, values are + * counts of items with that item status. + */ + protected function _avail_views() { + $avail_views = array(); + + foreach ( array( 'readonly' => true, 'writeable' => false ) as $view => $value ) { + $avail_views[ $view ] = count( wp_list_filter( $this->all_items, array( 'readonly' => $value ) ) ); + } + + return $avail_views; + } + + /** + * Retrieve an array of item states for an item. + * + * @since 0.1.0 + * + * @see WP_REST_API_List_Table::get_item_states() + * + * @param object $item The item to retrieve states for. + * @return array Array of item states. + */ + protected function _get_item_states( $item, $item_status ) { + $item_states = array(); + + if ( true === $item->readonly && 'readonly' !== $item_status ) { + $item_states['readonly'] = __( 'Readonly', 'shc-rest-api-inspector' ); + } + + return $item_states; + } + + /** + * Can a given item drill down into the "next" item? + * + * As of 0.1.0, we can't drill down further than properties. However, that may + * change in the future. For instance, if the type of a property can be `object`, + * then we'll need to implement drilling down into the properties of that property. + * I'm not sure if that is legal when registering routes. + * + * @since 0.1.0 + * + * @param object $item + * @return bool True if the given item can drill down into the "next" item; false otherwise. + */ + protected function has_view_next( $item ) { + return false; + } +} diff --git a/includes/admin/includes/class-wp-rest-routes-list-table.php b/includes/admin/includes/class-wp-rest-routes-list-table.php new file mode 100644 index 0000000..0105fad --- /dev/null +++ b/includes/admin/includes/class-wp-rest-routes-list-table.php @@ -0,0 +1,344 @@ + 'route', + 'plural' => 'routes', + ); + + $args = wp_parse_args( $args, $defaults ); + + parent::__construct( $args ); + +// $this->hierarchical_display = true; + } + + /** + * Get a list of columns. The format is: + * 'internal-name' => 'Title' + * + * @since 0.1.0 + * + * @return array + */ + function get_columns() { + $columns = array( + 'name' => __( 'Route', 'shc-rest-api-inspector' ), + 'parent' => __( 'Parent', 'shc-rest-api-inspector' ), + 'methods' => __( 'Methods', 'shc-rest-api-inspector' ), + 'namespace' => __( 'Namespace', 'shc-rest-api-inspector' ), + 'links' => __( 'Links', 'shc-rest-api-inspector' ), + 'endpoints' => __( 'Endpoints', 'shc-rest-api-inspector' ), + ); + + return parent::get_columns() + $columns; + } + + /** + * Get a list of sortable columns. The format is: + * 'internal-name' => 'orderby' + * or + * 'internal-name' => array( 'orderby', true ) + * + * The second format will make the initial sorting order be descending + * + * @since 0.1.0 + * + * @return array + */ + protected function get_sortable_columns() { + $columns = array( + 'namespace' => 'namespace', + 'endpoints' => 'endpoints', + ); + + return parent::get_sortable_columns() + $columns; + } + + /** + * Handles the namespace column output. + * + * @since 0.1.0 + * + * @param object $item The current route object. + */ + function column_namespace( $item ) { + return sprintf( + '%s', + add_query_arg( + array( + 'page' => $_REQUEST['page'], + 'namespace' => $item->namespace, + ), + admin_url( 'tools.php' ) + ), + $item->namespace + ); + } + + /** + * Handles the links column output. + * + * @since 0.1.0 + * + * @param object $item The current route object. + */ + function column_links( $item ) { + $links = array(); + + foreach ( $item->_links as $rel => $url ) { + $links[] = sprintf( + '%2$s %3$s', + esc_url( $url ), + esc_html( $rel ), + /* translators: Accessibility text. */ + __( '(opens in a new tab)' ) + ); + } + + return implode( '
', $links ); + } + + /** + * Handles the endpoints column output. + * + * @since 0.1.0 + * + * @param object $item The current route object. + */ + function column_endpoints( $item ) { + return count( $item->endpoints ); + } + + /** + * Output dropdowns. + * + * @since 0.1.0 + * + * @return void + */ + protected function do_dropdowns() { + $this->methods_dropdown(); + $this->namespaces_dropdown(); + $this->link_types_dropdown(); + + return; + } + + /** + * Display a link types dropdown for filtering items. + * + * @since 0.1.0 + * + * @eturn void + */ + protected function link_types_dropdown() { + $link_types = $options = array(); + + foreach ( $this->all_items as $item ) { + $link_types = array_merge( $link_types, array_keys( $item->_links ) ); + } + + if ( empty( $link_types ) ) { +// essentially, the same as the 'hide_if_empty' arg on core's category_dropdown(). +// return; + } + + foreach ( array_unique( $link_types ) as $link_rel ) { + $options[ $link_rel ] = $link_rel; + } + + $this->dropdown( + 'link_type', + __( 'Filter by link type', 'shc-rest-api-inspector' ), + __( 'All link types', 'shc-rest-api-inspector' ), + $options + ); + + return; + } + + /** + * Gext the text to display for the "View" row action. + * + * @since 0.1.0 + * + * @return string + */ + protected function get_view_text() { + return __( 'Endpoints', 'shc-rest-api-inspector' ); + } + + /** + * Retrieves the view endpoints link for a route. + * + * This method is similar to core's + * {@link https://developer.wordpress.org/reference/functions/get_edit_post_link/ get_edit_post_link()}. + * However, the analogy is pretty tenuous, so maybe it should be renamed to + * avoid confusion. + * + * @since 0.1.0 + * + * @param object $item The current endpoint item. + * @return string The view endpoints URL for the given route. + */ + protected function get_view_next_link( $item ) { + return add_query_arg( + array( + 'page' => $_REQUEST['page'], + 'route' => urlencode( $item->name ), + ), + admin_url( 'tools.php' ) + ); + } + + /** + * Retrieves the view schema link for a route. + * + * This method is similar to core's + * {@link https://developer.wordpress.org/reference/functions/get_edit_post_link/ get_edit_post_link()}. + * However, the analogy is pretty tenuous, so maybe it should be renamed to + * avoid confusion. + * + * @since 0.1.0 + * + * @param object $item The current endpoint item. + * @return string The view endpoints URL for the given route. + */ + protected function get_view_schema_link( $item ) { + return add_query_arg( + array( + 'page' => $_REQUEST['page'], + 'schema' => urlencode( $item->name ), + ), + admin_url( 'tools.php' ) + ); + } + + protected function _get_row_actions( $item ) { + $actions = array(); + + if ( ! empty( $item->schema ) ) { + $actions['view-schema'] = sprintf( + '%s', + $this->get_view_schema_link( $item ), + /* translators: %s: Post title. */ + esc_attr( sprintf( __( 'View Schema for “%s”' ), $item->name ) ), + __( 'Schema', 'shc-rest-api-inspector' ) + ); + + $handbook_url = $this->get_handbook_url( $item ); + if ( $handbook_url ) { + $actions['view-handbook'] = sprintf( + '%2$s %3$s', + esc_url( $handbook_url ), + esc_html( __( 'Handbook', 'shc-rest-api-inspector') ), + /* translators: Accessibility text. */ + __( '(opens in a new tab)' ) + ); + } + } + + return $actions; + } + + protected function get_handbook_url( $item ) { + $url = ''; + // this would be SOOOOO much easier if there were a 1-to-1 mapping between schema->title + // and the handbook URL :-( + switch ( $item->schema->title ) { + case 'post': + $url = 'posts'; + break; + case 'page': + $url = 'pages'; + break; + case 'post-revision': + case 'page-revision': + $url = 'post-revisions'; + break; + case 'attachment': + $url = 'media'; + break; + case 'wp_block': + $url = 'wp_blocks'; + break; + case 'wp_block-revision': + $url = 'wp_block-revisions'; + break; + case 'type': + $url = 'post-types'; + break; + case 'status': + $url = 'post-statuses'; + break; + case 'taxonomy': + $url = 'taxonomies'; + break; + case 'category': + $url = 'categories'; + break; + case 'tag': + $url = 'tags'; + break; + case 'user': + $url = 'users'; + break; + case 'comment': + $url = 'comments'; + break; + case 'search-result': + $url = 'search-results'; + break; + case 'rendered-block': + $url = 'rendered-blocks'; + break; + case 'settings': + $url = 'settings'; + break; + case 'theme': + $url = 'themes'; + break; + } + + if ( $url ) { + $url = "https://developer.wordpress.org/rest-api/reference/{$url}/"; + } + + return $url; + } +} diff --git a/includes/admin/includes/class-wp-rest-schema-links-list-table.php b/includes/admin/includes/class-wp-rest-schema-links-list-table.php new file mode 100644 index 0000000..4824127 --- /dev/null +++ b/includes/admin/includes/class-wp-rest-schema-links-list-table.php @@ -0,0 +1,115 @@ + 'schema_link', + 'plural' => 'schema_links', + ); + + $args = wp_parse_args( $args, $defaults ); + + parent::__construct( $args ); + } + + /** + * Get a list of columns. The format is: + * 'internal-name' => 'Title' + * + * @since 0.1.0 + * + * @return array + */ + function get_columns() { + $columns = array( + 'name' => __( 'Name', 'shc-rest-api-inspector' ), + 'href' => __( 'href', 'shc-rest-api-inspector' ), + 'title' => __( 'Title', 'shc-rest-api-inspector' ), + ); + + return $columns + parent::get_columns(); + } + + /** + * Display a contexts dropdown for filtering items. + * + * @since 0.1.0 + * + * @eturn void + */ + protected function formats_dropdown() { + $formats = $options = array(); + $formats = array_filter( wp_list_pluck( $this->all_items, 'format' ) ); + + foreach ( array_unique( $formats ) as $format ) { + $options[ $format ] = $format; + } + + $this->dropdown( + 'format', + __( 'Filter by format', 'shc-rest-api-inspector' ), + __( 'All formats', 'shc-rest-api-inspector' ), + $options + ); + + return; + } + + /** + * Can a given item drill down into the "next" item? + * + * As of 0.1.0, we can't drill down further than properties. However, that may + * change in the future. For instance, if the type of a property can be `object`, + * then we'll need to implement drilling down into the properties of that property. + * I'm not sure if that is legal when registering routes. + * + * @since 0.1.0 + * + * @param object $item + * @return bool True if the given item can drill down into the "next" item; false otherwise. + */ + protected function has_view_next( $item ) { + return false; + } + + protected function get_views() { + return array(); + } + + protected function _get_item_states( $item, $item_status ) { + return array(); + } +} diff --git a/includes/admin/includes/class-wp-rest-schema-properties-list-table.php b/includes/admin/includes/class-wp-rest-schema-properties-list-table.php new file mode 100644 index 0000000..3f79a92 --- /dev/null +++ b/includes/admin/includes/class-wp-rest-schema-properties-list-table.php @@ -0,0 +1,106 @@ + 'schema_property', + 'plural' => 'schema_properties', + ); + + $args = wp_parse_args( $args, $defaults ); + + parent::__construct( $args ); + } + + /** + * Get a list of columns. The format is: + * 'internal-name' => 'Title' + * + * @since 0.1.0 + * + * @return array + */ + function get_columns() { + $columns = array( + 'name' => __( 'Name', 'shc-rest-api-inspector' ), + 'type' => __( 'Type', 'shc-rest-api-inspector' ), + 'format' => __( 'Format', 'shc-rest-api-inspector' ), + 'description' => __( 'Description', 'shc-rest-api-inspector' ), + 'context' => __( 'Context', 'shc-rest-api-inspector' ), + ); + + return $columns + parent::get_columns(); + } + + /** + * Display a contexts dropdown for filtering items. + * + * @since 0.1.0 + * + * @eturn void + */ + protected function formats_dropdown() { + $formats = $options = array(); + $formats = array_filter( wp_list_pluck( $this->all_items, 'format' ) ); + + foreach ( array_unique( $formats ) as $format ) { + $options[ $format ] = $format; + } + + $this->dropdown( + 'format', + __( 'Filter by format', 'shc-rest-api-inspector' ), + __( 'All formats', 'shc-rest-api-inspector' ), + $options + ); + + return; + } + + /** + * Output dropdowns. + * + * @since 0.1.0 + * + * @return void + */ + protected function do_dropdowns() { + parent::do_dropdowns(); + $this->formats_dropdown(); + + return; + } +} diff --git a/includes/admin/includes/class-wp-rest-schemas-list-table.php b/includes/admin/includes/class-wp-rest-schemas-list-table.php new file mode 100644 index 0000000..c2f63ce --- /dev/null +++ b/includes/admin/includes/class-wp-rest-schemas-list-table.php @@ -0,0 +1,167 @@ + 'schema', + 'plural' => 'schemas', + ); + + $args = wp_parse_args( $args, $defaults ); + + parent::__construct( $args ); + } + + /** + * Get a list of columns. The format is: + * 'internal-name' => 'Title' + * + * @since 0.1.0 + * + * @return array + */ + function get_columns() { + $columns = array( + 'name' => __( 'Route', 'shc-rest-api-inspector' ), + 'title' => __( 'Title', 'shc-rest-api-inspector' ), + '$schema' => __( '$schema', 'shc-rest-api-inspector' ), +// 'links' => __( 'Links', 'shc-rest-api-inspector' ), + ); + + return parent::get_columns() + $columns; + } + + function column_links( $item ) { + return ''; + } + + protected function _get_row_actions( $item ) { + $actions = parent::_get_row_actions( $item ); + + if ( $item->links ) { + $actions['view-links'] = sprintf( + '%s', + $this->get_view_links_link( $item ), + /* translators: %s: Post title. */ + esc_attr( sprintf( __( 'View “%s”' ), $item->name ) ), + __( 'Links', 'shc-rest-api-inspector' ) + ); + } + + return $actions; + } + + protected function _get_item_states( $item, $item_status ) { + return array(); + } + + /** + * Can a given item drill down into the "next" item? + * + * Parameters can only drill down further if their type is `object`. + * + * @since 0.1.0 + * + * @param object $item + * @return bool True if the given item can drill down into the "next" item; false otherwise. + */ + protected function has_view_next( $item ) { + return ! empty( $item->properties ); + } + + /** + * Gext the text to display for the "View" row action. + * + * @since 0.1.0 + * + * @return string + */ + protected function get_view_text() { + return __( 'Properties', 'shc-rest-api-inspector' ); + } + + protected function get_views() { + return array(); + } + + /** + * Retrieves the view parameters link for an endpoint. + * + * This method is similar to core's + * {@link https://developer.wordpress.org/reference/functions/get_edit_post_link/ get_edit_post_link()}. + * However, the analogy is pretty tenuous, so maybe it should be renamed to + * avoid confusion. + * + * @since 0.1.0 + * + * @param object $item The current endpoint item. + * @return string The view parameters URL for the given endpoint. + */ + protected function get_view_next_link( $item ) { + return add_query_arg( + array( + 'page' => $_REQUEST['page'], + 'schema-properties' => urlencode( wp_unslash( $_REQUEST['schema'] ) ), + ), + admin_url( 'tools.php' ) + ); + } + + /** + * Retrieves the view parameters link for an endpoint. + * + * This method is similar to core's + * {@link https://developer.wordpress.org/reference/functions/get_edit_post_link/ get_edit_post_link()}. + * However, the analogy is pretty tenuous, so maybe it should be renamed to + * avoid confusion. + * + * @since 0.1.0 + * + * @param object $item The current endpoint item. + * @return string The view parameters URL for the given endpoint. + */ + protected function get_view_links_link( $item ) { + return add_query_arg( + array( + 'page' => $_REQUEST['page'], + 'schema-links' => urlencode( wp_unslash( $_REQUEST['schema'] ) ), + ), + admin_url( 'tools.php' ) + ); + } +} diff --git a/includes/admin/rest-api-endpoint-inspector.php b/includes/admin/rest-api-endpoint-inspector.php new file mode 100644 index 0000000..0f51f0d --- /dev/null +++ b/includes/admin/rest-api-endpoint-inspector.php @@ -0,0 +1,285 @@ +cap->view_items ) ) { + wp_die( + '

' . __( 'You need a higher level of permission.' ) . '

' . + '

' . __( 'Sorry, you are not allowed to view REST API Endpoints.', 'shc-rest-api-inspector' ) . '

', + 403 + ); +} + +$wp_list_table = Tool::_get_list_table( 'WP_REST_Endpoints_List_Table' ); + +$pagenum = $wp_list_table->get_pagenum(); + +// set $submenu_file, so that our tool sub-menu will get the 'current' CSS class. +// we have to declare these vars as global. +global $submenu_file; +$parent_file = "tools.php?page={$_REQUEST['page']}&noheader"; +$submenu_file = 'shc-rest-api-inspector'; + +$doaction = $wp_list_table->current_action(); + +if ( $doaction ) { + check_admin_referer( 'bulk-endpoints' ); + + $sendback = remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'locked', 'ids' ), wp_get_referer() ); + if ( ! $sendback ) { + $sendback = admin_url( $parent_file ); + } + $sendback = add_query_arg( 'paged', $pagenum, $sendback ); + + if ( ! empty( $_REQUEST['name'] ) ) { + $item_names = $_REQUEST['name']; + } + + if ( ! isset( $item_names ) ) { + wp_redirect( $sendback ); + exit; + } + + switch ( $doaction ) { + // we don't define any bulk actions, but other plugins could, so handle those. + default: + /** This action is documented in wp-admin/edit-comments.php */ + $sendback = apply_filters( 'handle_bulk_actions-' . get_current_screen()->id, $sendback, $doaction, $item_names ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + break; + } + + $sendback = remove_query_arg( array( 'action', 'action2', 'bulk_edit' ), $sendback ); + + wp_redirect( $sendback ); + exit(); +} elseif ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { + wp_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), wp_unslash( $_SERVER['REQUEST_URI'] ) ) ); + exit; +} + +$wp_list_table->prepare_items(); + +wp_enqueue_style( 'shc-rest-api-inspector-list-table' ); + +// we must declare $title as global so that get_admin_page_title() doesn't override it. +global $title; +$title = sprintf( + __( 'Endpoints for route “%s”', 'shc-rest-api-inspector' ), + esc_html( wp_unslash( $_GET['route'] ) ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'overview', + 'title' => __( 'Overview' ), + 'content' => + '

' . __( 'This screen provides access to all REST API Endpoints for a given route. You can customize the display of this screen to suit your workflow.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'screen-content', + 'title' => __( 'Screen Content' ), + 'content' => + '

' . __( 'You can customize the display of this screen’s contents in a number of ways:' ) . '

' . + '' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'action-links', + 'title' => __( 'Available Actions' ), + 'content' => + '

' . __( 'Hovering over a row in the endpoints list will display action links that allow you to manage your endpoint. You can perform the following actions:', 'shc-rest-api-inspector' ) . '

' . + '' . + '

' . __( 'Like all good list tables, additional row actions can be added with the rest_endpoint_row_actions and rest_item_row_actions filters.', 'shc-rest-api-inspector' ) + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'bulk-actions', + 'title' => __( 'Bulk Actions' ), + 'content' => + '

' . __( 'By default, this screen does not have any bulk actions.', 'shc-rest-api-inspector' ) . '

' . + '

' . __( 'However, they can be added using the rest-api-inspector-endpoint filter. If such custom bulk actions are added, then they will work just like bulk actions in any other list table-enabled screen.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'statuses', + 'title' => __( 'Statuses', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'Below is an explanation of the various statuses. These are the equivalent of "Published", "Pending", etc on the "All Posts" screen.', 'shc-rest-api-inspector' ) . '

' . + '
    ' . + '
  1. ' . __( 'Unauthenticated: the endpoint does not require authentication. This is is the case if there is no permissions_callback on the endpoint.', 'shc-rest-api-inspector' ) . '
  2. ' . + '
  3. ' . __( 'Authenticated: the endpoint requires authentication. This is is the case if there is a permissions_callback on the endpoint AND it does NOT have the GET method.', 'shc-rest-api-inspector' ) . '
  4. ' . + '
  5. ' . __( 'Maybe Authenticated: the endpoint MIGHT require authentication. This is the case if the endpoint has a permissions_callback AND the GET method, under the assumption that if the "context" is anything other than view it requires authentication, otherwise it MIGHT require authentication depending on how the permission_callback is written.', 'shc-rest-api-inspector' ) . '
  6. ' . + '
' . + '

' . __( 'Each status is also added as a "state" on each route. Unlike the "states" for the "All Posts" screen, each status is added as a state, whereas on the "All Posts" screen the "Published" status (i.e., the most prevalent status) is not added as a "state".', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'columns', + 'title' => __( 'Columns', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'Below is an explanation of the various columns:', 'shc-rest-api-inspector' ) . '

' . + '
    ' . + '
  1. ' . __( 'Route: Lists the regex for the route the endpoint is in.', 'shc-rest-api-inspector' ) . '
  2. ' . + '
  3. ' . __( 'Methods: Lists the methods of all endpoints for the route.', 'shc-rest-api-inspector' ) . '
  4. ' . + '
  5. ' . __( 'Callback: Has link to the WP Code Reference entry for the callback of the endpoint. See the "TODO\'s" tab for more details.', 'shc-rest-api-inspector' ) . '
  6. ' . + '
  7. ' . __( 'Permission Callback: Has link to the WP Code Reference entry for the permission_callback of the endpoint. See the "TODO\'s" tab for more details.', 'shc-rest-api-inspector' ) . '
  8. ' . + '
  9. ' . __( 'Show in Index: Lists whether the endpoint is shown in the index.', 'shc-rest-api-inspector' ) . '
  10. ' . + '
  11. ' . __( 'Accept JSON: Lists whether the endpoint accepts JSON.', 'shc-rest-api-inspector' ) . '
  12. ' . + '
  13. ' . __( 'Accept RAW: Lists whether the endpoint accepts RAW.', 'shc-rest-api-inspector' ) . '
  14. ' . + '
' . + '

' . __( 'The Methods column act like taxonomy columns on the "All Posts" screen. That is, they get dropdowns and their values are links that act like they were selected from the dropdown.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'search-columns', + 'title' => __( 'Search Columns', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'By default, the following columns are used when performing a search:', 'shc-rest-api-inspector' ) . '

' . + '
    ' . + '
  1. ' . __( 'Route', 'shc-rest-api-inspector' ) . + '
  2. ' . __( 'Methods', 'shc-rest-api-inspector' ) . + '
' . + '

' . __( 'That default can be changed with the rest_endpoint_search_column and rest_item_search_column filters.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'questions', + 'title' => __( 'Questions', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'I have the following questions about the implementation behind this screen:', 'shc-rest-api-inspector' ) . '

' . + '
    ' . + '
  1. ' . __( 'The information displayed on this screen is produced directly from massaging the output of WP_REST_Server::get_routes(), rather than parsing the JSON produced by the API on the front-end. Am I leaving anything out by doing it that way?', 'shc-rest-api-inspector' ) . '
  2. ' . + '
  3. ' . __( 'Is the method of determining whether an endpoint is authenticated correct (see the "Statuses" help tab)? I\'m pretty sure it is unreliable, for instance:', 'shc-rest-api-inspector' ) . + '
      ' . + '
    • ' . __( 'The endpoint for “/wp/v2/search” with method “GET” has status Maybe Authenticaed, yet it\'s permission_callback always returns true; hence, it\'s status should be Unauthenticaed.', 'shc-rest-api-inspector' ) . '
    • ' . + '
    • ' . __( 'The endpoint for “/wp/v2/users/me” with method “GET” has no permission_callback and hence has status Unauthenticated. Yet it\'s callback checks that the user is logged in (in essence, authenticating them). I can see the logic behind that, but it almost seems like a bug in the way that endpoint is defined.', 'shc-rest-api-inspector' ) . '
    • ' . + '
    • ' . __( 'A plugin could register an endpoint with method DELETE and it\'s status would be Authenticated. Yet, the there is nothing stopping that plugin from writing the permission_callback such that it always returns true, effectively making the endpoint Unauthenticated.', 'shc-rest-api-inspector' ) . '
    • ' . + '
    ' . + '
  4. ' . + '
  5. ' . __( 'I chose autenticated or not for the Status because that is important to me, but is there something else that would be better?', 'shc-rest-api-inspector' ) . + '
  6. ' . __( 'Should I add a Namespace column? Is it correct to say that an endpoint is in a namespace or are only routes in namespaces?', 'shc-rest-api-inspector' ) . + '
  7. ' . __( 'What do the values in the Accept JSON and Accept RAW columns mean?', 'shc-rest-api-inspector' ) . + '
  8. ' . __( 'As far as I can tell, all core endpoints have "Yes" for Show in Index and "No" for Accept JSON and Accept RAW. Therefore, I\'m wondering how useful these columns are.', 'shc-rest-api-inspector' ) . + '
  9. ' . __( 'Should I add a "view switcher" (like on the WP_MS_Sites_Table and WP_Media_List_Table)? If so, what should the other "mode" be?', 'shc-rest-api-inspector' ) . '
  10. ' . + '
  11. ' . __( 'Are there any other columns that should be added?', 'shc-rest-api-inspector' ) . + '
' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'todos', + 'title' => __( 'TODO\'s', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'The following are things that I eventually want to get to:', 'shc-rest-api-inspector' ) . '

' . + '' + ) +); + +get_current_screen()->set_help_sidebar( + '

' . __( 'For more information:' ) . '

' . + '

' . __( 'Documentation on the REST API', 'shc-rest-api-inspector' ) . '

' . + '

' . __( 'Support' ) . '

' +); + +add_screen_option( + 'per_page', + array( + 'default' => 20, + 'option' => 'endpoints_per_page', + ) +); + +require_once( ABSPATH . 'wp-admin/admin-header.php' ); + ?> +
+

+ +

+' . __( 'Search results for “%s”' ) . '', esc_attr( $_REQUEST['s'] ) ); +} +?> +

+ +

+
+ +views(); ?> + +
+search_box( $item_type_object->labels->search_items, 'rest_api_endpoint' ); ?> + + + + + + +display(); ?> + +
+
+
+
+ +cap->view_items ) ) { + wp_die( + '

' . __( 'You need a higher level of permission.' ) . '

' . + '

' . __( 'Sorry, you are not allowed to view REST API Parameters.', 'shc-rest-api-inspector' ) . '

', + 403 + ); +} + +$wp_list_table = Tool::_get_list_table( 'WP_REST_Parameters_List_Table' ); + +$pagenum = $wp_list_table->get_pagenum(); + +// set $submenu_file, so that our tool sub-menu will get the 'current' CSS class. +// we have to declare these vars as global. +global $submenu_file; +$parent_file = "tools.php?page={$_REQUEST['page']}&noheader"; +$submenu_file = 'shc-rest-api-inspector'; + +$doaction = $wp_list_table->current_action(); + +if ( $doaction ) { + check_admin_referer( 'bulk-parameters' ); + + $sendback = remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'locked', 'ids' ), wp_get_referer() ); + if ( ! $sendback ) { + $sendback = admin_url( $parent_file ); + } + $sendback = add_query_arg( 'paged', $pagenum, $sendback ); + + if ( ! empty( $_REQUEST['name'] ) ) { + $item_names = $_REQUEST['name']; + } + + if ( ! isset( $item_names ) ) { + wp_redirect( $sendback ); + exit; + } + + switch ( $doaction ) { + // we don't define any bulk actions, but other plugins could, so handle those. + default: + /** This action is documented in wp-admin/edit-comments.php */ + $sendback = apply_filters( 'handle_bulk_actions-' . get_current_screen()->id, $sendback, $doaction, $item_names ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + break; + } + + $sendback = remove_query_arg( array( 'action', 'action2', 'bulk_edit' ), $sendback ); + + wp_redirect( $sendback ); + exit(); +} elseif ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { + wp_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), wp_unslash( $_SERVER['REQUEST_URI'] ) ) ); + exit; +} + +$wp_list_table->prepare_items(); + +wp_enqueue_style( 'shc-rest-api-inspector-list-table' ); + +// we must declare $title as global so that get_admin_page_title() doesn't override it. +global $title; +$title = sprintf( + _n( + 'REST API Parameters for route “%s” with method “%s”', + 'REST API Parameters for route “%s” with methods “%s”', + count( explode( ' | ', $_GET['endpoint'] ) ), + 'shc-rest-api-inspector' + ), + esc_html( wp_unslash( $_GET['route'] ) ), + esc_html( wp_unslash( $_GET['endpoint'] ) ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'overview', + 'title' => __( 'Overview' ), + 'content' => + '

' . __( 'This screen provides access to all REST API Parameters for a given route and method. You can customize the display of this screen to suit your workflow.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'screen-content', + 'title' => __( 'Screen Content' ), + 'content' => + '

' . __( 'You can customize the display of this screen’s contents in a number of ways:' ) . '

' . + '' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'action-links', + 'title' => __( 'Available Actions' ), + 'content' => + '

' . __( 'Hovering over a row in the parameter list will display action links that allow you to manage your parameter. You can perform the following actions:', 'shc-rest-api-inspector' ) . '

' . + '' . + '

' . __( 'Like all good list tables, additional row actions can be added with the rest_parameter_row_actions and rest_item_row_actions filters.', 'shc-rest-api-inspector' ) + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'bulk-actions', + 'title' => __( 'Bulk Actions' ), + 'content' => + '

' . __( 'By default, this screen does not have any bulk actions.', 'shc-rest-api-inspector' ) . '

' . + '

' . __( 'However, they can be added using the rest-api-inspector-parameter filter. If such custom bulk actions are added, then they will work just like bulk actions in any other list table-enabled screen.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'statuses', + 'title' => __( 'Statuses', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'Below is an explanation of the various statuses. These are the equivalent of "Published", "Pending", etc on the "All Posts" screen.', 'shc-rest-api-inspector' ) . '

' . + '
    ' . + '
  1. ' . __( 'Required: The parameter is required :-)', 'shc-rest-api-inspector' ) . '
  2. ' . + '
  3. ' . __( 'Optional: The parameter is optional :-)', 'shc-rest-api-inspector' ) . '
  4. ' . + '
' . + '

' . __( 'Only the Required status is added as a "state" on each parameter, under the assumption that most parameters are optional. This is like on the "All Posts" screen, where the Published status is not added as a "state".', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'columns', + 'title' => __( 'Columns', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'Below is an explanation of the various columns:', 'shc-rest-api-inspector' ) . '

' . + '
    ' . + '
  1. ' . __( 'Name: Lists the name of the parameter.', 'shc-rest-api-inspector' ) . + '
  2. ' . __( 'Type: Lists the type(s) of the parameter. See the Questions help tab for questions about parameter types.', 'shc-rest-api-inspector' ) . + '
  3. ' . __( 'Array Items Type: Lists the type(s) of items in the array if the parameter has type array.', 'shc-rest-api-inspector' ) . + '
  4. ' . __( 'Description: Lists the description of the parameter.', 'shc-rest-api-inspector' ) . + '
  5. ' . __( 'Default: Lists the default value (if there is one) for the parameter.', 'shc-rest-api-inspector' ) . + '
  6. ' . __( 'Enum: Lists the enumerated values for the parameter if it\'s value must come from a fixed list.', 'shc-rest-api-inspector' ) . + '
' . + '

' . __( 'The Type and Array Item Types columns act like taxonomy columns on the "All Posts" screen. That is, they get a dropdowns and their values are links that act like they were selected from the dropdown.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'search-columns', + 'title' => __( 'Search Columns', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'By default, the following columns are used when performing a search:', 'shc-rest-api-inspector' ) . '

' . + '
    ' . + '
  1. ' . __( 'Name', 'shc-rest-api-inspector' ) . '
  2. ' . + '
  3. ' . __( 'Type', 'shc-rest-api-inspector' ) . '
  4. ' . + '
  5. ' . __( 'Array Items Type', 'shc-rest-api-inspector' ) . '
  6. ' . + '
  7. ' . __( 'Description', 'shc-rest-api-inspector' ) . '
  8. ' . + '
  9. ' . __( 'Default', 'shc-rest-api-inspector' ) . '
  10. ' . + '
  11. ' . __( 'Enum', 'shc-rest-api-inspector' ) . '
  12. ' . + '
' . + '

' . __( 'That default can be changed with the rest_parameter_search_column and rest_item_search_column filters.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'questions', + 'title' => __( 'Questions', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'I have the following questions about the implementation behind this screen:', 'shc-rest-api-inspector' ) . '

' . + '
    ' . + '
  1. ' . __( 'The information displayed on this screen is produced directly from massaging the output of WP_REST_Server::get_routes(), rather than parsing the JSON produced by the API on the front-end. Am I leaving anything out by doing it that way?', 'shc-rest-api-inspector' ) . '
  2. ' . + '
  3. ' . __( 'I chose required or not for the Status because that seemed the obvious choice, but is there something else that would be better?', 'shc-rest-api-inspector' ) . + '
  4. ' . __( 'What is the null type? As far as I can tell, it only occurs along with the string type.', 'shc-rest-api-inspector' ) . '
  5. ' . + '
  6. ' . __( 'As far as I can tell, the only parameters in core endpoints that have more than one type have string or null. Is it possible to declare a parameter to have more than one type where those "types" aren\'t string and null, e.g. array and boolean?', 'shc-rest-api-inspector' ) . '
  7. ' . + '
  8. ' . __( 'Some paramters in core endpoints have no type, e.g., context on the route “/wp/v2” with method “GET”. Is that just a "bug" in the way that parameter is registered in core? Or is it possible for plugins to register such parameters?', 'shc-rest-api-inspector' ) . '
  9. ' . + '
  10. ' . __( 'Is there a better name for the Enum column?', 'shc-rest-api-inspector' ) . + '
  11. ' . __( 'Should I add a "view switcher" (like on the WP_MS_Sites_Table and WP_Media_List_Table)? If so, what should the other "mode" be?', 'shc-rest-api-inspector' ) . '
  12. ' . + '
  13. ' . __( 'Are there any other columns that should be added?', 'shc-rest-api-inspector' ) . + '
' + ) +); + +get_current_screen()->set_help_sidebar( + '

' . __( 'For more information:' ) . '

' . + '

' . __( 'Documentation on the REST API', 'shc-rest-api-inspector' ) . '

' . + '

' . __( 'Support' ) . '

' +); + +add_screen_option( + 'per_page', + array( + 'default' => 20, + 'option' => 'parameter_per_page', + ) +); + +require_once( ABSPATH . 'wp-admin/admin-header.php' ); + ?> +
+

+ +

+' . __( 'Search results for “%s”' ) . '', esc_attr( $_REQUEST['s'] ) ); +} +?> +

+ +

+ +
+ +views(); ?> + +
+search_box( $item_type_object->labels->search_items, 'rest_api_parameter' ); ?> + + + + + + + +display(); ?> + +
+
+
+
+ +cap->view_items ) ) { + wp_die( + '

' . __( 'You need a higher level of permission.' ) . '

' . + '

' . __( 'Sorry, you are not allowed to view REST API Parameters.', 'shc-rest-api-inspector' ) . '

', + 403 + ); +} + +$wp_list_table = Tool::_get_list_table( 'WP_REST_Properties_List_Table' ); + +$pagenum = $wp_list_table->get_pagenum(); + +// set $submenu_file, so that our tool sub-menu will get the 'current' CSS class. +// we have to declare these vars as global. +global $submenu_file; +$parent_file = "tools.php?page={$_REQUEST['page']}&noheader"; +$submenu_file = 'shc-rest-api-inspector'; + +$doaction = $wp_list_table->current_action(); + +if ( $doaction ) { + check_admin_referer( 'bulk-properties' ); + + $sendback = remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'locked', 'ids' ), wp_get_referer() ); + if ( ! $sendback ) { + $sendback = admin_url( $parent_file ); + } + $sendback = add_query_arg( 'paged', $pagenum, $sendback ); + + if ( ! empty( $_REQUEST['name'] ) ) { + $item_names = $_REQUEST['name']; + } + + if ( ! isset( $item_names ) ) { + wp_redirect( $sendback ); + exit; + } + + switch ( $doaction ) { + // we don't define any bulk actions, but other plugins could, so handle those. + default: + /** This action is documented in wp-admin/edit-comments.php */ + $sendback = apply_filters( 'handle_bulk_actions-' . get_current_screen()->id, $sendback, $doaction, $item_names ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + break; + } + + $sendback = remove_query_arg( array( 'action', 'action2', 'bulk_edit' ), $sendback ); + + wp_redirect( $sendback ); + exit(); +} elseif ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { + wp_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), wp_unslash( $_SERVER['REQUEST_URI'] ) ) ); + exit; +} + +$wp_list_table->prepare_items(); + +wp_enqueue_style( 'shc-rest-api-inspector-list-table' ); + +// we must declare $title as global so that get_admin_page_title() doesn't override it. +global $title; +$title = sprintf( + _n( + 'REST API Properties for parameter “%s”, on route “%s” with method “%s”', + 'REST API Properties for parameter “%s”, on route “%s” with methods “%s”', + count( explode( ' | ', $_GET['endpoint'] ) ), + 'shc-rest-api-inspector' + ), + esc_html( wp_unslash( $_GET['parameter'] ) ), + esc_html( wp_unslash( $_GET['route'] ) ), + esc_html( wp_unslash( $_GET['endpoint'] ) ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'overview', + 'title' => __( 'Overview' ), + 'content' => + '

' . __( 'This screen provides access to all REST API Propertis for a given route, method and parameter. You can customize the display of this screen to suit your workflow.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'screen-content', + 'title' => __( 'Screen Content' ), + 'content' => + '

' . __( 'You can customize the display of this screen’s contents in a number of ways:' ) . '

' . + '' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'action-links', + 'title' => __( 'Available Actions' ), + 'content' => + '

' . __( 'Hovering over a row in the proprties list will display action links that allow you to manage your property. You can perform the following actions:', 'shc-rest-api-inspector' ) . '

' . + '' . + '

' . __( 'Like all good list tables, additional row actions can be added with the rest_property_row_actions and rest_item_row_actions filters.', 'shc-rest-api-inspector' ) + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'bulk-actions', + 'title' => __( 'Bulk Actions' ), + 'content' => + '

' . __( 'By default, this screen does not have any bulk actions.', 'shc-rest-api-inspector' ) . '

' . + '

' . __( 'However, they can be added using the rest-api-inspector-property filter. If such custom bulk actions are added, then they will work just like bulk actions in any other list table-enabled screen.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'statuses', + 'title' => __( 'Statuses', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'Below is an explanation of the various statuses. These are the equivalent of "Published", "Pending", etc on the "All Posts" screen.', 'shc-rest-api-inspector' ) . '

' . + '
    ' . + '
  1. ' . __( 'Readonly: The parameter is readonly :-)', 'shc-rest-api-inspector' ) . '
  2. ' . + '
  3. ' . __( 'Writable: The parameter is writable :-). I assume that writable properties only apply for methods other than GET.', 'shc-rest-api-inspector' ) . '
  4. ' . + '
' . + '

' . __( 'Only the Readonly status is added as a "state" on each parameter, under the assumption that most parameters are optional. This is like on the "All Posts" screen, where the Published status is not added as a "state".', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'columns', + 'title' => __( 'Columns', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'Below is an explanation of the various columns:', 'shc-rest-api-inspector' ) . '

' . + '
    ' . + '
  1. ' . __( 'Name: Lists the name of the property.', 'shc-rest-api-inspector' ) . + '
  2. ' . __( 'Type: Lists the type(s) of the property. See the Questions help tab for questions about property types.', 'shc-rest-api-inspector' ) . + '
  3. ' . __( 'Description: Lists the description of the property.', 'shc-rest-api-inspector' ) . + '
  4. ' . __( 'Context: Lists the context in which the property exists.', 'shc-rest-api-inspector' ) . + '
' . + '

' . __( 'The Type column acts like taxonomy columns on the "All Posts" screen. That is, it gets a dropdown and it\'s values are links that act like they were selected from the dropdown.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'search-columns', + 'title' => __( 'Search Columns', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'By default, the following columns are used when performing a search:', 'shc-rest-api-inspector' ) . '

' . + '
    ' . + '
  1. ' . __( 'Name', 'shc-rest-api-inspector' ) . + '
  2. ' . __( 'Type', 'shc-rest-api-inspector' ) . + '
  3. ' . __( 'Description', 'shc-rest-api-inspector' ) . + '
  4. ' . __( 'Context', 'shc-rest-api-inspector' ) . + '
' . + '

' . __( 'That default can be changed with the rest_property_search_column and rest_item_search_column filters.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'questions', + 'title' => __( 'Questions', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'I have the following questions about the implementation behind this screen:', 'shc-rest-api-inspector' ) . '

' . + '
    ' . + '
  1. ' . __( 'The information displayed on this screen is produced directly from massaging the output of WP_REST_Server::get_routes(), rather than parsing the JSON produced by the API on the front-end. Am I leaving anything out by doing it that way?', 'shc-rest-api-inspector' ) . '
  2. ' . + '
  3. ' . __( 'I chose readonly or writable for the Status because that seemed the obvious choice, but is there something else that would be better?', 'shc-rest-api-inspector' ) . + '
  4. ' . __( 'I haven\'t come across any properties in core endpoints where type has more than one value (like string and null for parameter types). Is it possible to register such properties?', 'shc-rest-api-inspector' ) . '
  5. ' . + '
  6. ' . __( 'Is it possible to register a property with type object or are only scalar or array types allowed? If so, the current implementation of this plugin will not allow you to drill down into property\'s object properties.', 'shc-rest-api-inspector' ) . '
  7. ' . + '
  8. ' . __( 'Some core endpoints register paramters with type object (e.g., the “meta”" property of route “/wp/v2/posts” with method “POST”) but do not specify any properties for that object type. Is that just a "bug" in the way that parameter is registered in core? Or is it possible for plugins to register such parameters?', 'shc-rest-api-inspector' ) . '
  9. ' . + '
  10. ' . __( 'Should I add a "view switcher" (like on the WP_MS_Sites_Table and WP_Media_List_Table)? If so, what should the other "mode" be?', 'shc-rest-api-inspector' ) . '
  11. ' . + '
  12. ' . __( 'Are there any other columns that should be added?', 'shc-rest-api-inspector' ) . + '
' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'todos', + 'title' => __( 'TODO\'s', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'The following are things that I eventually want to get to:', 'shc-rest-api-inspector' ) . '

' . + '' + ) +); + +get_current_screen()->set_help_sidebar( + '

' . __( 'For more information:' ) . '

' . + '

' . __( 'Documentation on the REST API', 'shc-rest-api-inspector' ) . '

' . + '

' . __( 'Support' ) . '

' +); + +add_screen_option( + 'per_page', + array( + 'default' => 20, + 'option' => 'properties_per_page', + ) +); + +require_once( ABSPATH . 'wp-admin/admin-header.php' ); + ?> +
+

+ +

+' . __( 'Search results for “%s”' ) . '', esc_attr( $_REQUEST['s'] ) ); +} +?> +

+ +

+
+ +views(); ?> + +
+search_box( $item_type_object->labels->search_items, 'rest_api_property' ); ?> + + + + + + + + +display(); ?> + +
+
+
+
+ +cap->view_items ) ) { + wp_die( + '

' . __( 'You need a higher level of permission.' ) . '

' . + '

' . __( 'Sorry, you are not allowed to view REST API Routes.' ) . '

', + 403 + ); +} + +$wp_list_table = Tool::_get_list_table( 'WP_REST_Routes_List_Table' ); + +$pagenum = $wp_list_table->get_pagenum(); + +// set $submenu_file, so that our tool sub-menu will get the 'current' CSS class. +// we have to declare these vars as global. +global $submenu_file; +$parent_file = "tools.php?page={$_REQUEST['page']}&noheader"; +$submenu_file = 'shc-rest-api-inspector'; + +$doaction = $wp_list_table->current_action(); + +if ( $doaction ) { + check_admin_referer( 'bulk-routes' ); + + $sendback = remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'locked', 'ids' ), wp_get_referer() ); + if ( ! $sendback ) { + $sendback = admin_url( $parent_file ); + } + $sendback = add_query_arg( 'paged', $pagenum, $sendback ); + + if ( ! empty( $_REQUEST['name'] ) ) { + $item_names = $_REQUEST['name']; + } + + if ( ! isset( $item_names ) ) { + wp_redirect( $sendback ); + exit; + } + + switch ( $doaction ) { + // we don't define any bulk actions, but other plugins could, so handle those. + default: + /** This action is documented in wp-admin/edit-comments.php */ + $sendback = apply_filters( 'handle_bulk_actions-' . get_current_screen()->id, $sendback, $doaction, $item_names ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + break; + } + + $sendback = remove_query_arg( array( 'action', 'action2', 'bulk_edit' ), $sendback ); + + wp_redirect( $sendback ); + exit(); +} elseif ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { + wp_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), wp_unslash( $_SERVER['REQUEST_URI'] ) ) ); + exit; +} + +$wp_list_table->prepare_items(); + +wp_enqueue_style( 'shc-rest-api-inspector-list-table' ); + +// we must declare $title as global so that get_admin_page_title() doesn't override it. +global $title; +$title = __( 'Routes', 'shc-rest-api-inspector' ); + +get_current_screen()->add_help_tab( + array( + 'id' => 'overview', + 'title' => __( 'Overview' ), + 'content' => + '

' . __( 'This screen provides access to all REST API Routes. You can customize the display of this screen to suit your workflow.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'screen-content', + 'title' => __( 'Screen Content' ), + 'content' => + '

' . __( 'You can customize the display of this screen’s contents in a number of ways:' ) . '

' . + '' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'action-links', + 'title' => __( 'Available Actions' ), + 'content' => + '

' . __( 'Hovering over a row in the routes list will display action links that allow you to manage your routes. You can perform the following actions:', 'shc-rest-api-inspector' ) . '

' . + '' . + '

' . __( 'Like all good list tables, additional row actions can be added with the rest_route_row_actions and rest_item_row_actions filters.', 'shc-rest-api-inspector' ) + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'bulk-actions', + 'title' => __( 'Bulk Actions' ), + 'content' => + '

' . __( 'By default, this screen does not have any bulk actions.', 'shc-rest-api-inspector' ) . '

' . + '

' . __( 'However, they can be added using the rest-api-inspector-route filter. If such custom bulk actions are added, then they will work just like bulk actions in any other list table-enabled screen.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'statuses', + 'title' => __( 'Statuses', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'Below is an explanation of the various statuses. These are the equivalent of "Published", "Pending", etc on the "All Posts" screen.', 'shc-rest-api-inspector' ) . '

' . + '
    ' . + '
  1. ' . __( 'Unauthenticated: no endpoints for the route require authentication. This is is the case if there is no permissions_callback on any endpoint for the route.', 'shc-rest-api-inspector' ) . '
  2. ' . + '
  3. ' . __( 'Authenticated: all endpoints for the route require authentication. This is is the case if there is a permissions_callback on all endpoints for the route AND no endpoint for the route has the GET method.', 'shc-rest-api-inspector' ) . '
  4. ' . + '
  5. ' . __( 'Maybe Authenticated: at least one endpoint for the route MIGHT require authentication. This is the case if at least one endpoint on the route has a permissions_callback AND the GET method.', 'shc-rest-api-inspector' ) . '
  6. ' . + '
' . + '

' . __( 'Each status is also added as a "state" on each route. Unlike the "states" for the "All Posts" screen, each status is added as a state, whereas on the "All Posts" screen the "Published" status (i.e., the most prevalent status) is not added as a "state".', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'columns', + 'title' => __( 'Columns', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'Below is an explanation of the various columns:', 'shc-rest-api-inspector' ) . '

' . + '
    ' . + '
  1. ' . __( 'Route: Lists the regex for the route.', 'shc-rest-api-inspector' ) . + '
  2. ' . __( 'Methods: Lists the methods of all endpoints for the route.', 'shc-rest-api-inspector' ) . + '
  3. ' . __( 'Namespace: Lists the namespace for the route.', 'shc-rest-api-inspector' ) . + '
  4. ' . __( 'Links: Lists the links for the route.', 'shc-rest-api-inspector' ) . + '
  5. ' . __( 'Endpoints: Lists the number of endpoints for the route. This is equivalent to the Posts column on the WP_Users_List_Table, e.g., the "All Users" screen.', 'shc-rest-api-inspector' ) . + '
' . + '

' . __( 'The Methods and Namespace columns act like taxonomy columns on the "All Posts" screen. That is, they get dropdowns and their values are links that act like they were selected from the dropdown.', 'shc-rest-api-inspector' ) . '

' . + '

' . __( 'The Links column does not act like taxonomy columns on the "All Posts" screen. Instead, clicking on a link in that column opens a new window/tab to display the REST API response for that link (having a JSON viewer plugin in your browser is very helpful for this :-). However, there is a Links dropdown that allows filtering routes by link type. See the "Questions" help tab for a question about this behavior.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'search-columns', + 'title' => __( 'Search Columns', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'By default, the following columns are used when performing a search:', 'shc-rest-api-inspector' ) . '

' . + '
    ' . + '
  1. ' . __( 'Route', 'shc-rest-api-inspector' ) . + '
  2. ' . __( 'Methods', 'shc-rest-api-inspector' ) . + '
  3. ' . __( 'Namespace', 'shc-rest-api-inspector' ) . + '
' . + '

' . __( 'That default can be changed with the rest_route_search_column and rest_item_search_column filters.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'questions', + 'title' => __( 'Questions', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'I have the following questions about the implementation behind this screen:', 'shc-rest-api-inspector' ) . '

' . + '
    ' . + '
  1. ' . __( 'The information displayed on this screen is produced directly from massaging the output of WP_REST_Server::get_routes(), rather than parsing the JSON produced by the API on the front-end. Am I leaving anything out by doing it that way?', 'shc-rest-api-inspector' ) . '
  2. ' . + '
  3. ' . __( 'Is the method of determining whether a route is authenticated correct (see the "Statuses" help tab)?', 'shc-rest-api-inspector' ) . '
  4. ' . + '
  5. ' . __( 'I chose autenticated or not for the Status because that is important to me, but is there something else that would be better?', 'shc-rest-api-inspector' ) . '
  6. ' . + '
  7. ' . __( 'Is this screen even necessary? It might make more sense to have the first screen list endpoints, where the columns are a merge of those from this screen and the Endpoints screen. In that case, the route would be listed in one row, and the methods would be indented in a "child" row (i.e., make the list table hierarchical, like WP_Posts_List_Table). Does that make sense?', 'shc-rest-api-inspector' ) . '
  8. ' . + '
  9. ' . __( 'Is the Endpoints column useful to anyone?', 'shc-rest-api-inspector' ) . '
  10. ' . + '
  11. ' . __( 'Some plugins (e.g., Gravity Forms) don\'t hook into rest_api_init when is_admin() === true; hence, their routes aren\'t displayed. Exploring non-core routes is one of the main reasons I started writing this plugin...so it\'s unfortunate that those routes aren\'t available. I don\'t know if there is any way around that.', 'shc-rest-api-inspector' ) . '
  12. ' . + '
  13. ' . __( 'Should I add a "view switcher" (like on the WP_MS_Sites_Table and WP_Media_List_Table)? If so, what should the other "mode" be?', 'shc-rest-api-inspector' ) . '
  14. ' . + '
  15. ' . __( 'Are there any other columns that should be added?', 'shc-rest-api-inspector' ) . + '
' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'todos', + 'title' => __( 'TODO\'s', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'The following are things that I eventually want to get to:', 'shc-rest-api-inspector' ) . '

' . + '' + ) +); + +get_current_screen()->set_help_sidebar( + '

' . __( 'For more information:' ) . '

' . + '

' . __( 'Documentation on the REST API', 'shc-rest-api-inspector' ) . '

' . + '

' . __( 'Support' ) . '

' +); + +add_screen_option( + 'per_page', + array( + 'default' => 20, + 'option' => 'route_per_page', + ) +); + +require_once( ABSPATH . 'wp-admin/admin-header.php' ); + ?> +
+

+ +

+' . __( 'Search results for “%s”' ) . '', esc_attr( $_REQUEST['s'] ) ); +} +?> +
+ +views(); ?> + +
+search_box( $item_type_object->labels->search_items, 'rest_api_route' ); ?> + + + + + +display(); ?> + +
+
+
+
+ +cap->view_items ) ) { + wp_die( + '

' . __( 'You need a higher level of permission.' ) . '

' . + '

' . __( 'Sorry, you are not allowed to view REST API Schemas.' ) . '

', + 403 + ); +} + +$wp_list_table = Tool::_get_list_table( 'WP_REST_Schemas_List_Table' ); + +$pagenum = $wp_list_table->get_pagenum(); + +// set $submenu_file, so that our tool sub-menu will get the 'current' CSS class. +// we have to declare these vars as global. +global $submenu_file; +$parent_file = "tools.php?page={$_REQUEST['page']}&noheader"; +$submenu_file = 'shc-rest-api-inspector'; + +$doaction = $wp_list_table->current_action(); + +if ( $doaction ) { + check_admin_referer( 'bulk-schemas' ); + + $sendback = remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'locked', 'ids' ), wp_get_referer() ); + if ( ! $sendback ) { + $sendback = admin_url( $parent_file ); + } + $sendback = add_query_arg( 'paged', $pagenum, $sendback ); + + if ( ! empty( $_REQUEST['name'] ) ) { + $item_names = $_REQUEST['name']; + } + + if ( ! isset( $item_names ) ) { + wp_redirect( $sendback ); + exit; + } + + switch ( $doaction ) { + // we don't define any bulk actions, but other plugins could, so handle those. + default: + /** This action is documented in wp-admin/edit-comments.php */ + $sendback = apply_filters( 'handle_bulk_actions-' . get_current_screen()->id, $sendback, $doaction, $item_names ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + break; + } + + $sendback = remove_query_arg( array( 'action', 'action2', 'bulk_edit' ), $sendback ); + + wp_redirect( $sendback ); + exit(); +} elseif ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { + wp_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), wp_unslash( $_SERVER['REQUEST_URI'] ) ) ); + exit; +} + +$wp_list_table->prepare_items(); + +wp_enqueue_style( 'shc-rest-api-inspector-list-table' ); + +// we must declare $title as global so that get_admin_page_title() doesn't override it. +global $title; +$title = sprintf( __( 'Schema for “%s”', 'shc-rest-api-inspector' ), wp_unslash( $_REQUEST['schema'] ) ); + +get_current_screen()->add_help_tab( + array( + 'id' => 'overview', + 'title' => __( 'Overview' ), + 'content' => + '

' . __( 'This screen provides access to the Schema for a route. You can customize the display of this screen to suit your workflow.', 'shc-rest-api-inspector' ) + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'todos', + 'title' => __( 'TODO\'s', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'The following are things that I eventually want to get to:', 'shc-rest-api-inspector' ) . '

' . + '' + ) +); + +get_current_screen()->set_help_sidebar( + '

' . __( 'For more information:' ) . '

' . + '

' . __( 'Documentation on the REST API', 'shc-rest-api-inspector' ) . '

' . + '

' . __( 'Support' ) . '

' +); + +add_screen_option( + 'per_page', + array( + 'default' => 20, + 'option' => 'schema_per_page', + ) +); + +require_once( ABSPATH . 'wp-admin/admin-header.php' ); + ?> +
+

+ +

+' . __( 'Search results for “%s”' ) . '', esc_attr( $_REQUEST['s'] ) ); +} +?> +
+ +views(); ?> + +
+search_box( $item_type_object->labels->search_items, 'rest_api_route' ); ?> + + + + + +display(); ?> + +
+
+
+
+ +cap->view_items ) ) { + wp_die( + '

' . __( 'You need a higher level of permission.' ) . '

' . + '

' . __( 'Sorry, you are not allowed to view REST API Parameters.', 'shc-rest-api-inspector' ) . '

', + 403 + ); +} + +$wp_list_table = Tool::_get_list_table( 'WP_REST_Schema_Links_List_Table' ); + +$pagenum = $wp_list_table->get_pagenum(); + +// set $submenu_file, so that our tool sub-menu will get the 'current' CSS class. +// we have to declare these vars as global. +global $submenu_file; +$parent_file = "tools.php?page={$_REQUEST['page']}&noheader"; +$submenu_file = 'shc-rest-api-inspector'; + +$doaction = $wp_list_table->current_action(); + +if ( $doaction ) { + check_admin_referer( 'bulk-schema_links' ); + + $sendback = remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'locked', 'ids' ), wp_get_referer() ); + if ( ! $sendback ) { + $sendback = admin_url( $parent_file ); + } + $sendback = add_query_arg( 'paged', $pagenum, $sendback ); + + if ( ! empty( $_REQUEST['name'] ) ) { + $item_names = $_REQUEST['name']; + } + + if ( ! isset( $item_names ) ) { + wp_redirect( $sendback ); + exit; + } + + switch ( $doaction ) { + // we don't define any bulk actions, but other plugins could, so handle those. + default: + /** This action is documented in wp-admin/edit-comments.php */ + $sendback = apply_filters( 'handle_bulk_actions-' . get_current_screen()->id, $sendback, $doaction, $item_names ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + break; + } + + $sendback = remove_query_arg( array( 'action', 'action2', 'bulk_edit' ), $sendback ); + + wp_redirect( $sendback ); + exit(); +} elseif ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { + wp_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), wp_unslash( $_SERVER['REQUEST_URI'] ) ) ); + exit; +} + +$wp_list_table->prepare_items(); + +wp_enqueue_style( 'shc-rest-api-inspector-list-table' ); + +// we must declare $title as global so that get_admin_page_title() doesn't override it. +global $title; +$title = sprintf( + __( 'REST API Schema Links for route “%s”', 'shc-rest-api-inspector' ), + esc_html( wp_unslash( $_GET['schema-links'] ) ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'overview', + 'title' => __( 'Overview' ), + 'content' => + '

' . __( 'This screen provides access to all the links in the schema for a given route. You can customize the display of this screen to suit your workflow.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'todos', + 'title' => __( 'TODO\'s', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'The following are things that I eventually want to get to:', 'shc-rest-api-inspector' ) . '

' . + '' + ) +); + +get_current_screen()->set_help_sidebar( + '

' . __( 'For more information:' ) . '

' . + '

' . __( 'Documentation on the REST API', 'shc-rest-api-inspector' ) . '

' . + '

' . __( 'Support' ) . '

' +); + +add_screen_option( + 'per_page', + array( + 'default' => 20, + 'option' => 'schema_property_per_page', + ) +); + +require_once( ABSPATH . 'wp-admin/admin-header.php' ); + ?> +
+

+ +

+' . __( 'Search results for “%s”' ) . '', esc_attr( $_REQUEST['s'] ) ); +} +?> +

+ +

+
+ +views(); ?> + + +
+
+
+ +cap->view_items ) ) { + wp_die( + '

' . __( 'You need a higher level of permission.' ) . '

' . + '

' . __( 'Sorry, you are not allowed to view REST API Parameters.', 'shc-rest-api-inspector' ) . '

', + 403 + ); +} + +$wp_list_table = Tool::_get_list_table( 'WP_REST_Schema_Properties_List_Table' ); + +$pagenum = $wp_list_table->get_pagenum(); + +// set $submenu_file, so that our tool sub-menu will get the 'current' CSS class. +// we have to declare these vars as global. +global $submenu_file; +$parent_file = "tools.php?page={$_REQUEST['page']}&noheader"; +$submenu_file = 'shc-rest-api-inspector'; + +$doaction = $wp_list_table->current_action(); + +if ( $doaction ) { + check_admin_referer( 'bulk-schema_properties' ); + + $sendback = remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'locked', 'ids' ), wp_get_referer() ); + if ( ! $sendback ) { + $sendback = admin_url( $parent_file ); + } + $sendback = add_query_arg( 'paged', $pagenum, $sendback ); + + if ( ! empty( $_REQUEST['name'] ) ) { + $item_names = $_REQUEST['name']; + } + + if ( ! isset( $item_names ) ) { + wp_redirect( $sendback ); + exit; + } + + switch ( $doaction ) { + // we don't define any bulk actions, but other plugins could, so handle those. + default: + /** This action is documented in wp-admin/edit-comments.php */ + $sendback = apply_filters( 'handle_bulk_actions-' . get_current_screen()->id, $sendback, $doaction, $item_names ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + break; + } + + $sendback = remove_query_arg( array( 'action', 'action2', 'bulk_edit' ), $sendback ); + + wp_redirect( $sendback ); + exit(); +} elseif ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { + wp_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), wp_unslash( $_SERVER['REQUEST_URI'] ) ) ); + exit; +} + +$wp_list_table->prepare_items(); + +wp_enqueue_style( 'shc-rest-api-inspector-list-table' ); + +// we must declare $title as global so that get_admin_page_title() doesn't override it. +global $title; +$title = sprintf( + __( 'REST API Schema Properties for route “%s”', 'shc-rest-api-inspector' ), + esc_html( wp_unslash( $_GET['schema-properties'] ) ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'overview', + 'title' => __( 'Overview' ), + 'content' => + '

' . __( 'This screen provides access to all the properties in the schema for a given route. You can customize the display of this screen to suit your workflow.', 'shc-rest-api-inspector' ) . '

' + ) +); + +get_current_screen()->add_help_tab( + array( + 'id' => 'todos', + 'title' => __( 'TODO\'s', 'shc-rest-api-inspector' ), + 'content' => + '

' . __( 'The following are things that I eventually want to get to:', 'shc-rest-api-inspector' ) . '

' . + '' + ) +); + +get_current_screen()->set_help_sidebar( + '

' . __( 'For more information:' ) . '

' . + '

' . __( 'Documentation on the REST API', 'shc-rest-api-inspector' ) . '

' . + '

' . __( 'Support' ) . '

' +); + +add_screen_option( + 'per_page', + array( + 'default' => 20, + 'option' => 'schema_property_per_page', + ) +); + +require_once( ABSPATH . 'wp-admin/admin-header.php' ); + ?> +
+

+ +

+' . __( 'Search results for “%s”' ) . '', esc_attr( $_REQUEST['s'] ) ); +} +?> +

+ +

+
+ +views(); ?> + +
+search_box( $item_type_object->labels->search_items, 'rest_api_schema_property' ); ?> + + + + + + +display(); ?> + +
+
+
+
+ + (object) array( 'view_items' => self::$view_items_cap ), + 'labels' => (object) array(), + ); + + switch ( $item_type ) { + case 'route': + $item_type_object->labels->search_items = __( 'Search Routes', 'shc-rest-api-inspector' ); + break; + case 'schema': + $item_type_object->labels->search_items = __( 'Search Schemas', 'shc-rest-api-inspector' ); + break; + case 'endpoint': + $item_type_object->labels->search_items = __( 'Search Endpoints', 'shc-rest-api-inspector' ); + break; + case 'parameter': + $item_type_object->labels->search_items = __( 'Search Parameters', 'shc-rest-api-inspector' ); + break; + case 'property': + case 'schema_property': + $item_type_object->labels->search_items = __( 'Search Properties', 'shc-rest-api-inspector' ); + break; + case 'schema_link': + $item_type_object->labels->search_items = __( 'Search Links', 'shc-rest-api-inspector' ); + break; + } + + return $item_type_object; + } +} diff --git a/includes/class-tool.php b/includes/class-tool.php new file mode 100644 index 0000000..c5eeacb --- /dev/null +++ b/includes/class-tool.php @@ -0,0 +1,246 @@ +hook_suffix = add_management_page( + // note: the page title will be set in the various admin/rest-api-{item_type}-inspector.php files. + '', + __( 'REST API Inspector', 'shc-rest-api-inspector' ), + REST_API_Item_Type::$view_items_cap, + Plugin::get_instance()->basename, + array( $this, 'render_tool_page' ) + ); + + add_action( "load-{$this->hook_suffix}", array( $this, 'maybe_add_noheader_arg' ) ); + + return; + } + + /** + * One the various `per_page` screen options. + * + * @since 0.1.0 + * + * @param bool $keep Whether to save or skip saving the screen option value. Default false. + * @param string $option The option name. + * @param int $value The number of rows to use. + * @return int|bool The number of rows to use, or the value of `$keep` if `$option`$this + * is not one our options. + * + * @filter set-screen-option + */ + function set_screen_option( $keep, $option, $value ) { + $our_screen_options = array( + 'route_per_page', + 'endpoint_per_page', + 'parameter_per_page', + 'property_per_page', + 'schema_property_per_page', + ); + if ( in_array( $option, $our_screen_options ) ) { + $status = $value; + } + + return $status; + } + + /** + * Setup the current screen. + * + * Modifies the `$base` and `$id` properties to be closer to what they would + * be if our screens were real core screens and not a tools page. Also + * adds a dynamic `$rest_item_type` property to the screen analogous to + * the `$post_type` property built-in the core screens. + * + * @since 0.1.0 + * + * @param WP_Screen $current_screen Current WP_Screen object. + * + * @action current_screen + */ + function current_screen( $current_screen ) { + global $current_screen; + + if ( $this->hook_suffix !== $current_screen->id ) { + // not our screen. + // nothing to do, so bail. + return; + } + + // determine the rest_item_type by what query parameters are set. + + if ( ! empty( $_REQUEST['schema-properties'] ) ) { + $current_screen->rest_item_type = 'schema_property'; + } + elseif ( ! empty( $_REQUEST['schema-links'] ) ) { + $current_screen->rest_item_type = 'schema_link'; + } + elseif ( ! empty( $_REQUEST['schema'] ) ) { + $current_screen->rest_item_type = 'schema'; + } + elseif ( empty( $_REQUEST['route'] ) ) { + $current_screen->rest_item_type = 'route'; + } + elseif ( empty( $_REQUEST['endpoint'] ) ) { + $current_screen->rest_item_type = 'endpoint'; + } + elseif ( empty( $_REQUEST['parameter'] ) ) { + $current_screen->rest_item_type = 'parameter'; + } + else { + $current_screen->rest_item_type = 'property'; + } + + $current_screen->base = 'rest-api-inspector'; + $current_screen->id = "{$current_screen->base}-{$current_screen->rest_item_type}"; + + return; + } + + /** + * Render our tool page. + * + * @since 0.1.0 + * + * @global string $rest_item_typenow The global REST item type (analogous to + * $typenow for post types). + * + * @return void + * + * @action $this->hook_suffix + */ + function render_tool_page() { + global $rest_item_typenow; + + $screen = get_current_screen(); + $rest_item_typenow = $screen->rest_item_type; + + require Plugin::get_instance()->dirname . "/includes/admin/rest-api-{$screen->rest_item_type}-inspector.php"; + + return; + } + + /** + * Ensure that the `noheader` query parameter is added to our URL. + * + * This is so that the various `admin/rest-api-{$item_type}-inspector.php` + * files will work propertly. + * + * @since 0.1.0 + * + * @return void + * + * @action load-{$this->hook_suffix} + */ + function maybe_add_noheader_arg() { + if ( ! isset( $_GET['noheader'] ) ) { + wp_redirect( add_query_arg( 'noheader', '' ) ); + + exit; + } + + return; + } + + /** + * Fetches an instance of a WP_REST_API_List_Table class. + * + * Based on {@link https://developer.wordpress.org/reference/functions/_get_list_table/ _get_list_table(). + * + * @access private + * @since 0.1.0 + * + * @global string $hook_suffix + * + * @param string $class The type of the list table, which is the class name. + * @param array $args Optional. Arguments to pass to the class. Accepts 'screen'. + * @return WP_REST_API_List_Table|bool List table object on success, false if the class does not exist. + */ + protected function _get_list_table( $class, $args = array() ) { + $class = __NAMESPACE__ . "\\{$class}"; + + if ( class_exists( $class ) ) { + if ( isset( $args['screen'] ) ) { + $args['screen'] = convert_to_screen( $args['screen'] ); + } elseif ( isset( $GLOBALS['hook_suffix'] ) ) { + $args['screen'] = get_current_screen(); + } else { + $args['screen'] = null; + } + + return new $class( $args ); + } + + return false; + } + + /** + * Register our scripts and styles. + * + * @since 0.1.0 + * + * @return void + * + * @action init + */ + function register_scripts() { + $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; + + wp_register_style( + Plugin::get_instance()->basename . '-list-table', + plugins_url( "assets/css/list-table{$suffix}.css", Plugin::get_instance()->file ), + array(), + Plugin::VERSION + ); + + return; + } +} diff --git a/includes/class-wp-rest-api-query.php b/includes/class-wp-rest-api-query.php new file mode 100644 index 0000000..f4aca66 --- /dev/null +++ b/includes/class-wp-rest-api-query.php @@ -0,0 +1,1130 @@ +prepare_query( $query ); + $this->query(); + } + } + + /** + * Fills in missing query variables with default values. + * + * @since 0.1.0 + * + * @param array $args Query vars, as passed to `WP_REST_API_Query`. + * @return array Complete query variables with undefined ones filled in with defaults. + */ + public static function fill_query_vars( $args ) { + $defaults = array( + 'item_type' => 'route', + 'item_status' => '', + 'endpoint' => '', + 'method' => '', + 'search' => '', + 'search_columns' => array(), + 'orderby' => '', + 'order' => 'ASC', + 'offset' => '', + 'number' => '', + 'paged' => 1, + 'count_total' => true, + 'fields' => 'all', + 'route' => '', + 'schema' => '', + 'type' => '', + 'namespace' => '', + 'parameter' => '', + 'context' => '', + 'array_items_type' => '', + 'link_type' => '', + 'format' => '', + ); + + return wp_parse_args( $args, $defaults ); + } + + /** + * Prepare the query variables. + * + * @since 0.1.0 + * + * @param string|array $query { + * Optional. Array or string of Query parameters. + * + * @todo finish documenting the hash. + * } + * @return void + */ + function prepare_query( $query = array() ) { + if ( empty( $this->query_vars ) || ! empty( $query ) ) { + $this->query_limit = null; + $this->query_vars = $this->fill_query_vars( $query ); + } + + /** + * Fires before the WP_REST_API_Query has been parsed. + * + * @since 0.1.0 + * + * @param WP_REST_API_Query $this The current WP_REST_API_Query instance, + * passed by reference. + */ + do_action( 'pre_get_rest_items', $this ); + + // Ensure that query vars are filled after 'pre_get_rest_items'. + $qv =& $this->query_vars; + $qv = $this->fill_query_vars( $qv ); + + if ( ! empty( $qv['item_status'] ) ) { + switch ( $qv['item_type'] ) { + case 'route': + case 'endpoint': + $qv['item_status_column'] = 'authenticated'; + + break; + case 'parameter': + $qv['item_status_column'] = 'required'; + $qv['item_status'] = 'required' === $qv['item_status']; + + break; + case 'property': + case 'schema_property': + $qv['item_status_column'] = 'readonly'; + $qv['item_status'] = 'readonly' === $qv['item_status']; + + break; + } + } + + // sorting + $qv['order'] = isset( $qv['order'] ) ? strtoupper( $qv['order'] ) : ''; + + $qv['search'] = strtolower( trim( $qv['search'] ) ); + + // fill in search_columns depending on the item_type. + if ( ! empty( $qv['search'] ) && empty( $qv['search_columns'] ) ) { + switch ( $qv['item_type'] ) { + case 'route': + $qv['search_columns'] = array( + 'scalar' => array( + 'name', + 'namespace', + ), + 'array' => array( + 'methods' + ), + ); + break; + case 'endpoint': + $qv['search_columns'] = array( + 'scalar' => array( + 'name', + ), + 'array' => array( + 'methods', + ) + ); + break; + case 'parameter': + $qv['search_columns'] = array( + 'scalar' => array( + 'name', + 'description', + ), + 'array' => array( + 'type', + 'array_items_type', + 'default_value', + 'enum', + ), + ); + break; + case 'property': + case 'schema_property': + $qv['search_columns'] = array( + 'scalar' => array( + 'name', + 'description', + 'format', + ), + 'array' => array( + 'type', + 'context', + ), + ); + break; + case 'schema_link': + $qv['search_columns'] = array( + 'scalar' => array( + 'name', + 'href', + 'title', + ), + 'array' => array( + ) + ); + break; + } + } + + // save the item type in a variable so the filter tag below are cleaner + // for documentation purposes. + $item_type = $qv['item_type']; + + /** + * Filters the columns to search in a WP_REST_API_Query search. + * + * The default columns depend on the item type. + * + * @since 0.1.0 + * + * @param string[] $search_columns Array of column names to be searched. + * @param string $search Text being searched. + * @param WP_REST_API_Query $this The current WP_REST_API_Query instance. + */ + $qv['search_columns'] = apply_filters( "rest_{$item_type}_search_columns", $qv['search_columns'], $qv['search'], $this ); + + /** + * Filters the columns to search in a WP_REST_API_Query search. + * + * The default columns depend on the item type. + * + * @since 0.1.0 + * + * @param string[] $search_columns Array of column names to be searched. + * @param string $item_type The item type being searched. + * @param string $search Text being searched. + * @param WP_REST_API_Query $this The current WP_REST_API_Query instance. + */ + $qv['search_columns'] = apply_filters( 'rest_item_search_columns', $qv['search_columns'], $qv['item_type'], $qv['search'], $this ); + + /** + * Fires after the WP_REST_API_Query has been parsed, and before + * the query is executed. + * + * @since 0.1.0 + * + * @param WP_REST_API_Query $this The current WP_REST_API_Query instance, + * passed by reference. + */ + do_action_ref_array( 'pre_rest_item_query', array( &$this ) ); + + return; + } + + /** + * Execute the query, with the current variables. + * + * @since 0.1.0 + * + * @return void + */ + function query() { + // I know it is unefficient to all of the route data at this point + // (including parameters and properties), + // without know whether we'll need it for this particular query (e.g., + // if the query is for routes w/ namespace = 'foo', we don't really need + // to massage the paramater data. + // however, when I first started this plugin, I didn't know a whole lot + // about what form the data contained in the rest server had, so I began by + // exploring that...and wrote the _get_routes() method during that time. + // when I started on this class, since I already had the data in that + // format I just went ahead and used it, so that I could work on the other + // aspects of this plugin (e.g., the list tables themselves). + // eventually, I'll get around to coming back to this and making this query + // class more efficient. + $this->_get_routes(); + + $qv =& $this->query_vars; + + /** + * Filters the items array before the query takes place. + * + * Return a non-null value to bypass WordPress's default REST API items queries. + * Filtering functions that require pagination information are encouraged to set + * the `total_users` property of the WP_REST_API_Query object, passed to the filter + * by reference. If WP_REST_API_Query does not perform a database query, it will not + * have enough information to generate these values itself. + * + * @since 0.1.0 + * + * @param array|null $results Return an array of item data to short-circuit WP's REST + * API item query or null to allow WP to run its normal queries. + * @param WP_REST_API_Query $this The WP_REST_API_Query instance (passed by reference). + */ + list( $this->all_results, $this->results ) = apply_filters_ref_array( 'rest_items_pre_query', array( null, &$this ) ); + + if ( null === $this->results ) { + $this->all_results = $this->results = array(); + + switch ( $this->query_vars['item_type'] ) { + case 'route': + $this->all_results = $this->results = $this->routes; + + break; + case 'schema': + if ( isset( $this->routes[ $qv['route'] ]->schema ) ) { + $this->all_results = $this->results = array( $this->routes[ $qv['route'] ]->schema ); + } + + break; + case 'schema_property': + if ( isset( $this->routes[ $qv['route'] ]->schema->properties ) ) { + $this->all_results = $this->results = $this->routes[ $qv['route'] ]->schema->properties; + } + + break; + case 'schema_link': + if ( isset( $this->routes[ $qv['route'] ]->schema->links ) ) { + $this->all_results = $this->results = $this->routes[ $qv['route'] ]->schema->links; + } + + break; + case 'endpoint': + if ( isset( $this->routes[ $qv['route'] ]->endpoints ) ) { + $this->all_results = $this->results = $this->routes[ $qv['route'] ]->endpoints; + } + + break; + case 'parameter': + if ( isset( $this->routes[ $qv['route'] ]->endpoints[ $qv['endpoint'] ]->args ) ) { + $this->all_results = $this->results = $this->routes[ $qv['route'] ]->endpoints[ $qv['endpoint'] ]->args; + } + + break; + case 'property': + if ( isset( $this->routes[ $qv['route'] ]->endpoints[ $qv['endpoint'] ]->args[ $qv['parameter'] ]->properties ) ) { + $this->all_results = $this->results = $this->routes[ $qv['route'] ]->endpoints[ $qv['endpoint'] ]->args[ $qv['parameter'] ]->properties; + } + + break; + } + + if ( ! empty( $qv['item_status'] ) ) { + $this->results = $this->filter_scalar_equal( $this->results, $qv['item_status_column'], $qv['item_status'] ); + } + + if ( ! empty( $qv['namespace'] ) ) { + $this->results = $this->filter_scalar_equal( $this->results, 'namespace' ); + } + + if ( ! empty( $qv['method'] ) ) { + $this->results = $this->filter_in_array( $this->results, 'methods', $qv['method'] ); + } + + if ( ! empty( $qv['link_type'] ) ) { + $this->results = $this->filter_in_array_keys( $this->results, 'links', $qv['link_type'] ); + } + + if ( ! empty( $qv['type'] ) ) { + $this->results = $this->filter_in_array( $this->results, 'type' ); + } + + if ( ! empty( $qv['context'] ) ) { + $this->results = $this->filter_in_array( $this->results, 'context' ); + } + + if ( ! empty( $qv['format'] ) ) { + $this->results = $this->filter_scalar_equal( $this->results, 'format' ); + } + + if ( ! empty( $qv['array_items_type'] ) ) { + $this->results = $this->filter_in_array( $this->results, 'array_items_type' ); + } + + // further refine the results if a column search is to be performed. + if ( ! empty( $qv['search'] ) ) { + $this->results = $this->filter_search( $this->results, $qv['search_columns'], $qv['search'] ); + } + + // order the results. + if ( ! empty( $qv['orderby'] ) ) { + uasort( + $this->results, + function( $a, $b ) use ( $qv ) { + $a = $a->{$qv['orderby']}; + $b = $b->{$qv['orderby']}; + + return 'ASC' === $qv['order'] ? $a <=> $b : $b <=> $a; + } + ); + } + + if ( isset( $qv['count_total'] ) && $qv['count_total'] ) { + $this->total_items = count( $this->results ); + } + } + + // limit + if ( $this->results && isset( $qv['number'] ) && $qv['number'] > 0 ) { + $this->results = array_slice( $this->results, $qv['offset'], $qv['number'] ); + } + + return; + } + + /** + * Return the list of items. + * + * @since 0.1.0 + * + * @return object[] Array of results. + */ + function get_results() { + return $this->results; + } + + /** + * Return the list of all items. + * + * @since 0.1.0 + * + * @return object[] Array of results. + */ + function get_all_results() { + return $this->all_results; + } + + /** + * Return the total number of items for the current query. + * + * @since 0.1.0 + * + * @return int Number of total items. + */ + function get_total() { + return $this->total_items; + } + + /** + * Parse an 'order' query variable and cast it to ASC or DESC as necessary. + * + * @since 0.1.0 + * + * @param string $order The 'order' query variable. + * @return string The sanitized 'order' query variable. + */ + protected function parse_order( $order ) { + if ( ! is_string( $order ) || empty( $order ) ) { + return 'DESC'; + } + + if ( 'ASC' === strtoupper( $order ) ) { + return 'ASC'; + } else { + return 'DESC'; + } + } + + /** + * Filter an array of items by whether a scalar-valued field has a specific value. + * + * This is essentially the same as calling `wp_list_filter()`, but since + * we need to do other kinds of "filter" operations that `wp_list_filter()` + * doesn't support, it makes sense to do all of them the same way (and not + * use `wp_list_filter()` at all. + * + * @since 0.1.0 + * + * @param object[] $items Array of items to filter. + * @param string $field The field to filter by. + * @param string $value The value to filter by. Default: `$this->query_vars[ $field ]`. + * @return object[] Array of filtered items. + */ + protected function filter_scalar_equal( $items, $field, $value = '' ) { + if ( ! $value ) { + $value = $this->query_vars[ $field ]; + } + + return array_filter( + $items, + function( $item ) use ( $field, $value ) { + // note the use of weak comparison...just in case. + return $value == $item->{$field}; + } + ); + } + + /** + * Filter an array of items by whether an array-valued field contains a + * specific value. + * + * @since 0.1.0 + * + * @param object[] $items Array of items to filter. + * @param string $field The field to filter by. + * @param string $value The query var to filter by. Default: the same as `$field`. + * @return object[] Array of filtered items. + */ + protected function filter_in_array( $items, $field, $value = '' ) { + if ( ! $value ) { + $value = $this->query_vars[ $field ]; + } + + return array_filter( + $items, + function( $item ) use ( $field, $value ) { + return in_array( $value, $item->{$field} ); + } + ); + } + + /** + * Filter an array of items by whether the keys of an array-valued field contain a + * specific value. + * + * @since 0.1.0 + * + * @param object[] $items Array of items to filter. + * @param string $field The field to filter by. + * @param string $value The query var to filter by. Default: the same as `$field`. + * @return object[] Array of filtered items. + */ + protected function filter_in_array_keys( $items, $field, $value = '' ) { + if ( ! $value ) { + $value = $this->query_vars[ $field ]; + } + + return array_filter( + $items, + function( $item ) use ( $field, $value ) { + return in_array( $value, array_keys( $item->{$field} ) ); + } + ); + } + + /** + * Filter an array of items by whether the intersection of an array-valued + * field and an array-valued value is non-empty. + * + * @since 0.1.0 + * + * @param object[] $items Array of items to filter. + * @param string $field The field to filter by. + * @param string $value The query var to filter by. Default: the same as `$field`. + * @return object[] Array of filtered items. + */ + protected function filter_array_intersect( $items, $field, $value = '' ) { + if ( ! $value ) { + $value = $this->query_vars[ $field ]; + } + + return array_filter( + $items, + function( $item ) use ( $field, $value ) { + return ! empty( array_intersect( $item->{$field}, $value ) ); + } + ); + } + + /** + * Filter an array of items by whether a scalar valued field contains + * a specific sub-string value. + * + * @since 0.1.0 + * + * @param object[] $items Array of items to filter. + * @param string $field The field to filter by. + * @param string $value The query var to filter by. Default: the same as `$field`. + * @return object[] Array of filtered items. + */ + protected function filter_scalar_strpos( $items, $field, $value = '' ) { + if ( ! $value ) { + $value = $this->query_vars[ $field ]; + } + + return array_filter( + $items, + function( $item ) use ( $field, $value ) { + return false !== strpos( strtolower( $item->{$field} ), $value ); + } + ); + } + + /** + * Filter an array of items by whether any value in an array-valued + * field contains a specific sub-string value. + * + * @since 0.1.0 + * + * @param object[] $items Array of items to filter. + * @param string $field The field to filter by. + * @param string $value The query var to filter by. Default: the same as `$field`. + * @return object[] Array of filtered items. + */ + protected function filter_array_strpos( $items, $field, $value = '' ) { + if ( ! $value ) { + $value = $this->query_vars[ $field ]; + } + + return array_filter( + $items, + function( $item ) use ( $field, $value ) { + foreach ( $item->{$field} as $field_value ) { + if ( false !== strpos( strtolower( $field_value ), $value ) ) { + return true; + } + } + return false; + } + ); + } + + /** + * Filter an array of items by whether any value in it's search_columns + * contains a specific sub-string value. + * + * @since 0.1.0 + * + * @param object[] $items Array of items to filter. + * @param array $fields { + * Optional. The fields to filter by. Default: `$this->query_vars['search_columns']`. + * + * @type array $scalar An array of scalar-valued fields. + * @type array $array An array of array-valued fields. + * } + * @param string $value The value to search for. Default: `$this->query_vars['search']. + * @return object[] Array of filtered items. + */ + protected function filter_search( $items, $fields = '', $value = '' ) { + if ( ! $fields ) { + $fields = $this->query_vars['search_columns']; + } + if ( ! $value ) { + $value = $this->query_vars['search']; + } + + foreach ( $fields as $type => $fields ) { + foreach ( $fields as $field ) { + switch ( $type ) { + case 'scalar': + $_items = $this->filter_scalar_strpos( $this->results, $field, $value ); + if ( $_items ) { + return $_items; + } + + break; + case 'array': + $_items = $this->filter_array_strpos( $this->results, $field, $value ); + if ( $_items ) { + return $_items; + } + + break; + } + } + } + + return array(); + } + + /** + * Massage the return value of + * {@link https://developer.wordpress.org/reference/classes/wp_rest_server/get_routes/ WP_REST_Server::get_routes()} + * into a form that makes querying and displaying them in a list table easier. + * + * @since 0.1.0 + * + * @return void + * + * @todo see if there are functions/methods in core that would make this massaging easier. + */ + protected function _get_routes() { + $rest_server = rest_get_server(); + + foreach ( $rest_server->get_routes() as $name => $endpoints ) { + $this->routes[ $name ] = $this->get_data_for_route( + $name, + $endpoints, + $rest_server->get_route_options( $name ) + ); + } + + // now, setup the hierarchy of routes. + foreach ( $this->routes as $name => $route ) { + if ( '/' === $name ) { + continue; + } + $parents = array_filter( + array_keys( $this->routes ), + function ( $_route ) use ( $route ) { + return '/' !== $_route && $_route !== $route->name && false !== strpos( $route->name, $_route ); + } + ); + + if ( ! empty( $parents ) ) { + $route->parent = end( $parents ); + } + + $i=0; + } + + return; + } + + /** + * Retrieves all data for a route. + * + * Similar in spirit to + * {@link https://developer.wordpress.org/reference/classes/wp_rest_server/get_data_for_route/ WP_REST_Server::get_data_for_route()} + * but includes non-public data and massaged in such a way to make querying and displaying + * in a list table easier. + * + * @since 0.1.0 + * + * @param string $route Route to get data for. + * @param array $endpoints Endpoints to convert to data. + * @param array $options Options for the route. + * @return object { + * Data for the route. + * + * @type string $item_type 'route'. + * @type string $name The name (i.e., path regex) of the route. + * @type string $authenticated The authentication status of the route. + * One of 'yes', 'no' or 'maybe'. + * @type string $namespace The namespace of the route. + * @type string[] $methods The methods supported by the route. + * @type object[] $endpoints Array of endpoints for the route. + * Keys are the method(s) of the endpoint, + * For endpoints with more than one method, + * the key is `implode( ' | ', $methods )`. + * @type object $schema The schema for the route. + * @type string[] $_links The links for the route. Keys are the "rel" type + * of the link. + * } + */ + protected function get_data_for_route( $route, $endpoints, $options ) { + $data = array( + 'item_type' => 'route', + 'name' => $route, + 'authenticated' => 'maybe', + 'namespace' => '', + 'methods' => array(), + 'endpoints' => array(), + 'schema' => array(), + '_links' => array(), + 'parent' => '', + ); + + if ( isset( $options['namespace'] ) ) { + $data['namespace'] = $options['namespace']; + } + + foreach ( $endpoints as $endpoint ) { + $endpoint['name'] = $route; + $endpoint_data = $this->get_data_for_endpoint( $endpoint ); + $methods = implode( ' | ', $endpoint_data->methods ); + $data['endpoints'][ $methods ] = $endpoint_data; + } + + $data['methods'] = array_keys( $data['endpoints'] ); + + if ( isset( $options['schema'] ) ) { + $data['schema'] = $this->get_data_for_schema( $route, call_user_func( $options['schema'] ) ); + } + + // For non-variable routes, generate links. + if ( 0 === preg_match( '#\(\?P<(\w+?)>.*?\)#', $route ) ) { + $data['_links'] = array( + 'self' => rest_url( $route ), + ); + } + + $authenticated = array_values( wp_list_pluck( $data['endpoints'], 'authenticated' ) ); + $authenticated = array_unique( $authenticated ); + + if ( array( 'yes' ) === $authenticated ) { + $data['authenticated'] = 'yes'; + } + elseif ( array( 'no' ) === $authenticated ) { + $data['authenticated'] = 'no'; + } + + return (object) $data; + } + + /** + * Retrieves all data for a schema. + * + * @since 0.1.0 + * + * @param array $endpoint Endpoint to get data for. + * @return object { + * Data for the endpoint. + * + * @type string $item_type 'endpoint'. + * @type string $name The name (i.e., path regex) of the route the endpoint is in. + * @type string $authenticated The authentication status of the endpoint. + * One of 'yes', 'no' or 'maybe'. + * @type string[] $methods The methods supported by the endpoint. + * @type object[] $args Array of parameters for the endpoint. + * Keys are the names of the paramters. + * @type callable $callback The get/create/update/delete callback for the endpoint. + * @type object[] $args The parameters for the endpoint. + * @type bool $accept_json ????? + * @type bool $accept_raw ????? + * @type bool $show_in_index True if the endpoint is shown in the index, false otherwise. + * } + */ + protected function get_data_for_schema( $route, $schema ) { + $defaults = array( + 'item_type' => 'schema', + '$schema' => '', + 'name' => $route, + 'title' => '', + 'type' => '', + 'properties' => array(), + 'links' => array(), + ); + $data = wp_parse_args( $schema, $defaults ); + + foreach ( $data['properties'] as $name => $property ) { + $property['name'] = $name; + $data['properties'][ $name ] = $this->get_data_for_schema_property( $property ); + } + + foreach ( $data['links'] as $idx => $link ) { + $link = $this->get_data_for_schema_link( $link ); + $data['links'][ $link->name ] = $link; + unset( $data['links'][ $idx ] ); + } + + return (object) $data; + } + + /** + * Retrieves all data for a schema link. + * + * @since 0.1.0 + * + * @param array $endpoint Endpoint to get data for. + * @return object { + * Data for the endpoint. + * + * @type string $item_type 'endpoint'. + * @type string $name The name (i.e., path regex) of the route the endpoint is in. + * @type string $authenticated The authentication status of the endpoint. + * One of 'yes', 'no' or 'maybe'. + * @type string[] $methods The methods supported by the endpoint. + * @type object[] $args Array of parameters for the endpoint. + * Keys are the names of the paramters. + * @type callable $callback The get/create/update/delete callback for the endpoint. + * @type object[] $args The parameters for the endpoint. + * @type bool $accept_json ????? + * @type bool $accept_raw ????? + * @type bool $show_in_index True if the endpoint is shown in the index, false otherwise. + * } + */ + protected function get_data_for_schema_link( $link ) { + $defaults = array( + 'item_type' => 'schema_link', + 'title' => '', + 'name' => $link['rel'], + 'href' => '', + 'targetSchema' => array(), + ); + $data = wp_parse_args( $link, $defaults ); + unset( $data['rel'] ); + +// foreach ( $data['targetSchema'] as $name => $property ) { +// $property['name'] = $name; +// $data['properties'][ $name ] = $this->get_data_for_schema_property( $property ); +// } + + return (object) $data; + } + + /** + * Retrieves all data for an endpoint. + * + * @since 0.1.0 + * + * @param array $endpoint Endpoint to get data for. + * @return object { + * Data for the endpoint. + * + * @type string $item_type 'endpoint'. + * @type string $name The name (i.e., path regex) of the route the endpoint is in. + * @type string $authenticated The authentication status of the endpoint. + * One of 'yes', 'no' or 'maybe'. + * @type string[] $methods The methods supported by the endpoint. + * @type object[] $args Array of parameters for the endpoint. + * Keys are the names of the paramters. + * @type callable $callback The get/create/update/delete callback for the endpoint. + * @type object[] $args The parameters for the endpoint. + * @type bool $accept_json ????? + * @type bool $accept_raw ????? + * @type bool $show_in_index True if the endpoint is shown in the index, false otherwise. + * } + */ + protected function get_data_for_endpoint( $endpoint ) { + $defaults = array( + 'item_type' => 'endpoint', + 'name' => '', + 'authenticated' => 'maybe', + 'methods' => array(), + 'callback' => '', + 'permission_callback' => '', + 'args' => array(), + 'accept_json' => false, + 'accept_raw' => false, + 'show_in_index' => false, + ); + $data = wp_parse_args( $endpoint, $defaults ); + $data['methods'] = array_keys( $data['methods'] ); + + foreach ( $data['args'] as $name => $arg ) { + $arg['name'] = $name; + $data['args'][ $name ] = $this->get_data_for_arg( $arg ); + } + + if ( empty( $data['permission_callback'] ) ) { + $data['authenticated'] = 'no'; + } + elseif ( ! in_array( 'GET', $data['methods'] ) ) { + $data['authenticated'] = 'yes'; + } + + return (object) $data; + } + + /** + * Retrieves all data for a parameter. + * + * @since 0.1.0 + * + * @param array $arg Parameter to get data for. + * @return object { + * Data for the parameter. + * + * @type string $item_type 'parameter'. + * @type string $name The name of the parameter. + * @type bool $requried True if the parameter is requried, false otherwise. + * @type string[] $type The type(s) of the parameter. + * @type string[] $array_items_type Type of items if the parameter is an array type. + * @type string $description The description of the parameter. + * @type string|number|bool $default_value The default value of the parameter. + * @type string|number|bool[] $enum The enumerated values of the parameter, if it's + * value must come from a fixed list. + * @type object[] $properties The properties of the parameter if it's type is 'object', + * empty array otherwise. + * } + */ + protected function get_data_for_arg( $arg ) { + $defaults = array( + 'item_type' => 'parameter', + 'name' => '', + 'required' => false, + 'type' => '', + 'array_items_type'=> array(), + 'description' => '', + 'default' => array(), + 'enum' => array(), + 'properties' => array(), + ); + + $data = wp_parse_args( $arg, $defaults ); + + if ( ! empty( $data['items'] ) ) { + $defaults = array( + 'type' => array(), + 'enum' => array(), + ); + + $data['items'] = wp_parse_args( $data['items'], $defaults ); + + if ( ! is_array( $data['items']['type'] ) ) { + $data['items']['type'] = array( $data['items']['type'] ); + } + + $data['array_items_type'] = $data['items']['type']; + $data['enum'] = $data['items']['enum']; + + unset( $data['items'] ); + } + + foreach ( $data['properties'] as $name => $property ) { + $property['name'] = $name; + $data['properties'][ $name ] = $this->get_data_for_property( $property ); + } + + if ( ! is_array( $data['type'] ) ) { + $data['type'] = array( $data['type'] ); + } + + // strip out "empty string" types. + $data['type'] = array_filter( $data['type'] ); + + if ( ! is_array( $data['default'] ) ) { + $data['default'] = array( $data['default'] ); + } + + // rename the "default" field to "default_value" to make it + // easier to display in a list table, i.e., avoid a conflict + // with WP_List_Table::column_default() which outputs column + // values for columns that don't have their own + // column_{$column_name} method. + $data['default_value'] = $data['default']; + unset( $data['default'] ); + + return (object) $data; + } + + /** + * Retrieves all data for a schema property. + * + * @since 0.1.0 + * + * @param array $property Parameter to get data for. + * @return object { + * Data for the parameter. + * + * @type string $item_type 'parameter'. + * @type string $name The name of the parameter. + * @type bool $requried True if the parameter is requried, false otherwise. + * @type string[] $type The type(s) of the parameter. + * @type string[] $array_items_type Type of items if the parameter is an array type. + * @type string $description The description of the parameter. + * @type string|number|bool $default_value The default value of the parameter. + * @type string|number|bool[] $enum The enumerated values of the parameter, if it's + * value must come from a fixed list. + * @type object[] $properties The properties of the parameter if it's type is 'object', + * empty array otherwise. + * } + */ + protected function get_data_for_schema_property( $property ) { + $defaults = array( + 'item_type' => 'schema_property', + 'name' => '', + 'readonly' => false, + 'type' => array(), + 'description' => '', + 'format' => '', + 'context' => array(), + ); + + $data = wp_parse_args( $property, $defaults ); + + if ( ! is_array( $data['type'] ) ) { + $data['type'] = array( $data['type'] ); + } + + return (object) $data; + } + + /** + * Retrieves all data for a property. + * + * @since 0.1.0 + * + * @param array $property Property to get data for. + * @return object { + * Data for the property. + * + * @type string $item_type 'property'. + * @type string $name The name of the property. + * @type bool $readonly True if the property is readonly, false otherwise. + * @type string[] $type The type(s) of the property. + * @type string $description The description of the property. + * @type string[] $context The contexts the property is available in. + * } + */ + protected function get_data_for_property( $property ) { + $defaults = array( + 'item_type' => 'property', + 'name' => '', + 'readonly' => false, + 'type' => array(), + 'description' => '', + 'context' => array(), + ); + $data = wp_parse_args( $property, $defaults ); + + // @todo can a property have 'array' or 'object' as it's type? + // core endpoints don't seem to have any such propoperties. + // but if plugins can register them we'll have to add additional + // functionality to handle those cases. + if ( ! is_array( $data['type'] ) ) { + $data['type'] = array( $data['type'] ); + } + + return (object) $data; + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bedd1d8 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "wp-rest-api-inspector", + "plugin_name": "REST API Inspector", + "version": "0.1.0", + "description": "Inspect the REST API routes, endpoints, parameters and properties registered on a site", + "author": "Paul V. Biron/Sparrow Hawk Computing", + "repository": "https://github.com/pbiron", + "namespace": "SHC\\REST_API_Inspector", + "tested_up_to": "5.3.2", + "engines": { + "node": "10.13.0" + }, + "license": "GPL-2.0-or-later", + "license_uri": "https://www.gnu.org/licenses/gpl-2.0.html", + "devDependencies": { + "grunt": "^1.0.3", + "grunt-composer": "^0.4.5", + "grunt-contrib-clean": "^1.1.0", + "grunt-contrib-copy": "^1.0.0", + "grunt-contrib-cssmin": "^2.2.1", + "grunt-contrib-jshint": "^1.1.0", + "grunt-contrib-sass": "^1.0.0", + "grunt-contrib-uglify-es": "^3.3.0", + "grunt-contrib-watch": "^1.1.0", + "grunt-rtlcss": "^2.0.1", + "grunt-text-replace": "^0.4.0", + "grunt-wp-readme-to-markdown": "^2.0.1", + "grunt-zip": "^0.18.1" + }, + "dependencies": {} +} diff --git a/plugin.php b/plugin.php new file mode 100644 index 0000000..0da9d4d --- /dev/null +++ b/plugin.php @@ -0,0 +1,70 @@ + REST API Inspector` menu that presents information about registered REST API routes, endpoints, etc in various list tables. + +== On Screen Help == + +There is skeletal help in the WP `Help` tabs on each screen, so I won't bother to say much here about how to get started and use this plugin: Just go to `Tools > REST API Inspector` and see the "Help" tab. + +Note: although it may look like there is only one screen (on that Tools menu), each "type" of data (e.g., routes, endpoints, parameters, properties) is displayed in a list table specific to that data "type"...with their own WP Screen instance. So, be sure to check the `Help` tabs on each such screen (especially the `Questions` tabs). + +Some of the on screen help may be incorrect...as I've added new functionality I haven't always had time to update the help tabs (e.g., "Available Actions" tab on the Routes screen doesn't mention the `Schema` and `Handbook` row actions). But, hey, this is only version 0.1.0 of the plugin, so you'll just have to figure some things out for yourselves :-) + +== Implementation == + +The implementation is *very* preliminary...but basically: + +* there is a `WP_REST_API_List_Table` (in the plugin's PHP namespace, but named so that in the unlikely event folks want this in core, things will be easier :-) + * and various sub-classes of that (e.g., `WP_REST_Routes_List_Table`, `WP_REST_Endpoints_List_Table`, etc) + * these are in `includes/wp-admin/includes` (again, the directory names were chosen to make moving into core easier if that should ever happen) + * there are also various `rest-api-routes-inspector.php`, `rest-api-endpoints-inspector.php`, etc in `includes\wp-admin` that serve the same purpose as core's `wp-admin/edit.php`, `wp-admin/users.php`, etc. + +* there is a rudimentary `WP_REST_API_Query` class that is loosely based on core's `WP_User_Query` + * this class is used by `WP_REST_API_List_Table::prepare_items()`, just like the analogous core query classes are used by the various core list tables + * this class is **GROSSLY** inefficient at this point. I'll work on that in later revisions + +I'm 100% sure there are bugs...but hopefully not major ones at this point. + +I'm also about 90% sure that: + +* I misunderstand some things about the route data returned by [WP_REST_Server::get_routes()](https://developer.wordpress.org/reference/classes/wp_rest_server/get_routes/) (which is where the info that is displayed on all of the screens comes from) +* therefore, some of what is displayed is either `wrong`, `misleading`, `not useful`, etc + +If you want to browse the code, most of it is fairly well documented, with ample DocBlocks (some of which have correct information :-). + +== How Can I Help? == + +1. Just try the plugin out! See if you can find any bugs. +2. Let me know if you have any suggestions for changes in functionality (either additions or changes to current functionality). +3. See the various `Questions` help tabs, and provide feedback on those questions. + +And finally, answer the big question: Is this plugin something that is worth continuing to work on? That is, does it provide useful information for users/developers (or could it in the future with changes)? + +The best way to provide feedback is to open an [Issue](https://github.com/pbiron/wp-rest-api-inspector) on GitHub. + +== Installation == + +From your WordPress dashboard + +1. Go to _Plugins > Add New_ and click on _Upload Plugin_ +2. Upload the zip file +3. Activate the plugin + +Also, if you have the awesome [GitHub Updater](https://github.com/afragen/github-updater/) plugin activated, you'll be able to update the plugin when new versions are released, right in the WP Dashboard (or via WP-CLI). + += Build from sources = + +1. clone the global repo to your local machine +2. install node.js and npm ([instructions](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)) +3. install composer ([instructions](https://getcomposer.org/download/)) +4. run `npm update` +5. run `composer install` +6. run `grunt build` + * to build a new release zip + * run `grunt release` + +**Important Note:** The build process is a kind of brittle at this point. Why? Because the main composer dependency (a set of "framework" classes I use for writing pllugins) is in a private GitHub repo. So, best *not* to do the `composer install` (or `composer update`) step above :-) + +== Changelog == + += 0.1.0 = + +* init commit diff --git a/uninstall.php b/uninstall.php new file mode 100644 index 0000000..f0dcece --- /dev/null +++ b/uninstall.php @@ -0,0 +1,9 @@ +