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' ) . '
' . __( 'You can customize the display of this screen’s contents in a number of ways:' ) . '
' .
+ '
' .
+ '
' . __( 'You can hide/display columns based on your needs and decide how many endpoints to list per screen using the Screen Options tab.', 'shc-rest-api-inspector' ) . '
' .
+ '
' . __( 'You can filter the list of endpoints by whether they are authenticated or not using the text links above the list to only show endpoints with that authentication type. The default view is to show all endpoints.', 'shc-rest-api-inspector' ) . '
' .
+ '
' . __( 'You can refine the list to show only endpoints with a specifc method by using the dropdown menu above the list. Click the Filter button after making your selection. You also can refine the list by clicking on the method in the list.', 'shc-rest-api-inspector' ) . '
' . __( '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' ) . '
' .
+ '
' .
+ '
' . __( 'View takes you to the parameters screen for that endpoint. You can also reach that screen by clicking on the endpoint name.', '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' ) . '
' . __( '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' ) . '
' .
+ '' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( 'Maybe Authenticated: the endpoint MIGHT require authentication. This is the case if the endpoint has a permissions_callbackAND 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' ) . '
' .
+ '' .
+ '
' . __( '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' ) . '
' . __( 'Below is an explanation of the various columns:', 'shc-rest-api-inspector' ) . '
' .
+ '' .
+ '
' . __( 'Route: Lists the regex for the route the endpoint is in.', 'shc-rest-api-inspector' ) . '
' .
+ '
' . __( 'Methods: Lists the methods of all endpoints for the route.', 'shc-rest-api-inspector' ) . '
' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( 'Show in Index: Lists whether the endpoint is shown in the index.', 'shc-rest-api-inspector' ) . '
' . __( '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' ) . '
' . __( 'I have the following questions about the implementation behind this screen:', 'shc-rest-api-inspector' ) . '
' .
+ '' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( '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' ) . '
' .
+ '
' .
+ '
' .
+ '
' . __( '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' ) .
+ '
' . __( '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' ) .
+ '
' . __( 'What do the values in the Accept JSON and Accept RAW columns mean?', 'shc-rest-api-inspector' ) .
+ '
' . __( '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' ) .
+ '
' . __( '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' ) . '
' .
+ '
' . __( 'Are there any other columns that should be added?', 'shc-rest-api-inspector' ) .
+ '
' . __( 'The following are things that I eventually want to get to:', 'shc-rest-api-inspector' ) . '
' .
+ '
' .
+ '
' . __( 'figure out a reliable way to distinquish core from non-core endpoints and just output the permission_callback "name" for non-core endpoints.', 'shc-rest-api-inspector' ) .
+ '
' . __( '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' ) . '
' . __( 'You can customize the display of this screen’s contents in a number of ways:' ) . '
' .
+ '
' .
+ '
' . __( 'You can hide/display columns based on your needs and decide how many parameters to list per screen using the Screen Options tab.', 'shc-rest-api-inspector' ) . '
' .
+ '
' . __( 'You can filter the list of parameters by whether they are required or not using the text links above the list to only show parameters with that required type. The default view is to show all parameters.', 'shc-rest-api-inspector' ) . '
' .
+ '
' . __( 'You can refine the list to show only parameters with a specifc type by using the dropdown menu above the list. Click the Filter button after making your selection. You also can refine the list by clicking on the type in the list.', 'shc-rest-api-inspector' ) . '
' . __( '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' ) . '
' .
+ '
' .
+ '
' . __( 'View takes you to the properties screen for that paremter. You can also reach that screen by clicking on the parameter name. Only parameters of type object can be viewed.', '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' ) . '
' . __( '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' ) . '
' .
+ '' .
+ '
' . __( 'Required: The parameter is required :-)', 'shc-rest-api-inspector' ) . '
' .
+ '
' . __( 'Optional: The parameter is optional :-)', 'shc-rest-api-inspector' ) . '
' .
+ '' .
+ '
' . __( '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' ) . '
' . __( 'Below is an explanation of the various columns:', 'shc-rest-api-inspector' ) . '
' .
+ '' .
+ '
' . __( 'Name: Lists the name of the parameter.', 'shc-rest-api-inspector' ) .
+ '
' . __( 'Type: Lists the type(s) of the parameter. See the Questions help tab for questions about parameter types.', 'shc-rest-api-inspector' ) .
+ '
' . __( 'Array Items Type: Lists the type(s) of items in the array if the parameter has type array.', 'shc-rest-api-inspector' ) .
+ '
' . __( 'Description: Lists the description of the parameter.', 'shc-rest-api-inspector' ) .
+ '
' . __( 'Default: Lists the default value (if there is one) for the parameter.', 'shc-rest-api-inspector' ) .
+ '
' . __( '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' ) . '
' . __( 'I have the following questions about the implementation behind this screen:', 'shc-rest-api-inspector' ) . '
' .
+ '' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( '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' ) .
+ '
' . __( 'What is the null type? As far as I can tell, it only occurs along with the string type.', 'shc-rest-api-inspector' ) . '
' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( 'Is there a better name for the Enum column?', 'shc-rest-api-inspector' ) .
+ '
' . __( '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' ) . '
' .
+ '
' . __( 'Are there any other columns that should be added?', 'shc-rest-api-inspector' ) .
+ '
' . __( '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' ) . '
' . __( 'You can customize the display of this screen’s contents in a number of ways:' ) . '
' .
+ '
' .
+ '
' . __( 'You can hide/display columns based on your needs and decide how many properties to list per screen using the Screen Options tab.', 'shc-rest-api-inspector' ) . '
' .
+ '
' . __( 'You can filter the list of properties by whether they are readonly or not using the text links above the list to only show properties with that readonly type. The default view is to show all properties.', 'shc-rest-api-inspector' ) . '
' .
+ '
' . __( 'You can refine the list to show only properties with a specifc type or context by using the dropdown menu above the list. Click the Filter button after making your selection. You also can refine the list by clicking on the type or context in the list.', 'shc-rest-api-inspector' ) . '
' . __( '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' ) . '
' .
+ '
' .
+ '
' . __( 'View takes you to the properties screen for that paremter. You can also reach that screen by clicking on the parameter name. Only parameters of type object can be viewed.', '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' ) . '
' . __( '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' ) . '
' .
+ '' .
+ '
' . __( 'Readonly: The parameter is readonly :-)', 'shc-rest-api-inspector' ) . '
' .
+ '
' . __( 'Writable: The parameter is writable :-). I assume that writable properties only apply for methods other than GET.', 'shc-rest-api-inspector' ) . '
' .
+ '' .
+ '
' . __( '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' ) . '
' . __( 'Below is an explanation of the various columns:', 'shc-rest-api-inspector' ) . '
' .
+ '' .
+ '
' . __( 'Name: Lists the name of the property.', 'shc-rest-api-inspector' ) .
+ '
' . __( 'Type: Lists the type(s) of the property. See the Questions help tab for questions about property types.', 'shc-rest-api-inspector' ) .
+ '
' . __( 'Description: Lists the description of the property.', 'shc-rest-api-inspector' ) .
+ '
' . __( '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' ) . '
' . __( 'I have the following questions about the implementation behind this screen:', 'shc-rest-api-inspector' ) . '
' .
+ '' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( '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' ) .
+ '
' . __( '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' ) . '
' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( 'Are there any other columns that should be added?', 'shc-rest-api-inspector' ) .
+ '
' . __( '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' ) . '
' . __( 'You can customize the display of this screen’s contents in a number of ways:' ) . '
' .
+ '
' .
+ '
' . __( 'You can hide/display columns based on your needs and decide how many routes to list per screen using the Screen Options tab.', 'shc-rest-api-inspector' ) . '
' .
+ '
' . __( 'You can filter the list of routes by whether they are authenticated or not using the text links above the list to only show routes with that authentication type. The default view is to show all routes.', 'shc-rest-api-inspector' ) . '
' .
+ '
' . __( 'You can refine the list to show only routes with a specifc method or namespace by using the dropdown menus above the list. Click the Filter button after making your selection. You also can refine the list by clicking on the method or namespace in the list.', 'shc-rest-api-inspector' ) . '
' . __( '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' ) . '
' .
+ '
' .
+ '
' . __( 'View takes you to the endpoints screen for that route. You can also reach that screen by clicking on the route name.', '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' ) . '
' . __( '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' ) . '
' .
+ '' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( '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_callbackAND the GET method.', 'shc-rest-api-inspector' ) . '
' .
+ '' .
+ '
' . __( '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' ) . '
' . __( 'Below is an explanation of the various columns:', 'shc-rest-api-inspector' ) . '
' .
+ '' .
+ '
' . __( 'Route: Lists the regex for the route.', 'shc-rest-api-inspector' ) .
+ '
' . __( 'Methods: Lists the methods of all endpoints for the route.', 'shc-rest-api-inspector' ) .
+ '
' . __( 'Namespace: Lists the namespace for the route.', 'shc-rest-api-inspector' ) .
+ '
' . __( 'Links: Lists the links for the route.', 'shc-rest-api-inspector' ) .
+ '
' . __( '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' ) . '
' . __( 'I have the following questions about the implementation behind this screen:', 'shc-rest-api-inspector' ) . '
' .
+ '' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( 'Is the method of determining whether a route is authenticated correct (see the "Statuses" help tab)?', 'shc-rest-api-inspector' ) . '
' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( 'Is the Endpoints column useful to anyone?', 'shc-rest-api-inspector' ) . '
' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( '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' ) . '
' .
+ '
' . __( 'Are there any other columns that should be added?', 'shc-rest-api-inspector' ) .
+ '
' . __( 'The following are things that I eventually want to get to:', 'shc-rest-api-inspector' ) . '
' .
+ '
' .
+ '
' . __( 'Make the items in this list table hierarchical, so that, for instance, routes “/oembed/1.0/embed” and “/oembed/1.0/proxy” would appear indented under route “/oembed/1.0” like child pages on the "All Pages" screen.', 'shc-rest-api-inspector' ) .
+ '
' . __( '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' ) . '
' .
+ '
' .
+ '
' . __( 'Write the content for the other help tabs for this screen.', 'shc-rest-api-inspector' ) .
+ '
' . __( 'Not sure how useful this screen is. Since there will only ever be one schema per route, displaying it in a list table is prtty superfluous. May be better to just have "Schema Properties and Schema Links row actions on the route list table?', 'shc-rest-api-inspector' ) .
+ '
' . __( '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' ) . '
' . __( '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' ) . '