├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── lib ├── Site.php └── namespace.php └── www ├── details.php ├── error.php ├── index.php ├── layout.php └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Ryan McCue 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WordPress API Discovery 2 | 3 | This library allows autodiscovery of the WordPress REST API shipping in WordPress 4.4. 4 | 5 | ## Installing 6 | 7 | This library should be installed via Composer as `wp-api/discovery`. 8 | 9 | If manually installing (i.e. via `git clone`), be sure to run `composer install` after cloning the code. 10 | 11 | ## Using 12 | 13 | The main entry point is the `WordPress\Discovery\discover()` function. Simply pass in a URL to discover the API. 14 | 15 | ```php 16 | /** 17 | * Discover the WordPress API from a URI. 18 | * 19 | * @param string $uri URI to start the search from. 20 | * @param bool $legacy Should we check for the legacy API too? 21 | * @return Site|null Site data if available, null if not a WP site. 22 | */ 23 | function discover( $uri, $legacy = false ) { 24 | ``` 25 | 26 | This project also includes a demo web install: 27 | 28 | ```sh 29 | php -S 0.0.0.0:9000 www/index.php 30 | ``` 31 | 32 | Then access http://localhost:9000/ to view the demo. It looks something like this: 33 | 34 | 35 | 36 | ## License 37 | 38 | This project is licensed under the MIT license. See [LICENSE.md](LICENSE.md) for the full license. 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-api/discovery", 3 | "license": "MIT", 4 | "require": { 5 | "rmccue/requests": "^1.6" 6 | }, 7 | "autoload": { 8 | "psr-4": { 9 | "WordPress\\Discovery\\": "lib/" 10 | }, 11 | "files": [ 12 | "lib/namespace.php" 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "d6cdcbb5d77148f4254ffe854572760c", 8 | "content-hash": "56c71077c404681471147c558c1fd67a", 9 | "packages": [ 10 | { 11 | "name": "rmccue/requests", 12 | "version": "v1.6.1", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/rmccue/Requests.git", 16 | "reference": "6aac485666c2955077d77b796bbdd25f0013a4ea" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/rmccue/Requests/zipball/6aac485666c2955077d77b796bbdd25f0013a4ea", 21 | "reference": "6aac485666c2955077d77b796bbdd25f0013a4ea", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": ">=5.2" 26 | }, 27 | "require-dev": { 28 | "satooshi/php-coveralls": "dev-master" 29 | }, 30 | "type": "library", 31 | "autoload": { 32 | "psr-0": { 33 | "Requests": "library/" 34 | } 35 | }, 36 | "notification-url": "https://packagist.org/downloads/", 37 | "license": [ 38 | "ISC" 39 | ], 40 | "authors": [ 41 | { 42 | "name": "Ryan McCue", 43 | "homepage": "http://ryanmccue.info" 44 | } 45 | ], 46 | "description": "A HTTP library written in PHP, for human beings.", 47 | "homepage": "http://github.com/rmccue/Requests", 48 | "keywords": [ 49 | "curl", 50 | "fsockopen", 51 | "http", 52 | "idna", 53 | "ipv6", 54 | "iri", 55 | "sockets" 56 | ], 57 | "time": "2014-05-18 04:59:02" 58 | } 59 | ], 60 | "packages-dev": [], 61 | "aliases": [], 62 | "minimum-stability": "stable", 63 | "stability-flags": [], 64 | "prefer-stable": false, 65 | "prefer-lowest": false, 66 | "platform": [], 67 | "platform-dev": [] 68 | } 69 | -------------------------------------------------------------------------------- /lib/Site.php: -------------------------------------------------------------------------------- 1 | data = $data; 31 | $this->index = $index; 32 | } 33 | 34 | /** 35 | * Get the name of a site. 36 | * 37 | * @return string 38 | */ 39 | public function getName() { 40 | return $this->data->name; 41 | } 42 | 43 | /** 44 | * Get the description for a site. 45 | * 46 | * @return string 47 | */ 48 | public function getDescription() { 49 | return $this->data->description; 50 | } 51 | 52 | /** 53 | * Get the URL for a site. 54 | * 55 | * @return string 56 | */ 57 | public function getURL() { 58 | return $this->data->url; 59 | } 60 | 61 | /** 62 | * Get the index URL for the API. 63 | * 64 | * @return string 65 | */ 66 | public function getIndexURL() { 67 | return $this->index; 68 | } 69 | 70 | /** 71 | * Get namespaces supported by the site. 72 | * 73 | * @return string[] List of namespaces supported by the site. 74 | */ 75 | public function getSupportedNamespaces() { 76 | if ( empty( $this->data->namespaces ) || ! is_array( $this->data->namespaces ) ) { 77 | return array(); 78 | } 79 | 80 | return $this->data->namespaces; 81 | } 82 | 83 | /** 84 | * Does the site support a namespace? 85 | * 86 | * @param string $namespace Namespace to check. 87 | * @return bool True if supported by the site, false otherwise. 88 | */ 89 | public function supportsNamespace( $namespace ) { 90 | return in_array( $namespace, $this->getSupportedNamespaces() ); 91 | } 92 | 93 | /** 94 | * Get features supported by the site. 95 | * 96 | * @return array Map of authentication method => method-specific data. 97 | */ 98 | public function getSupportedAuthentication() { 99 | if ( empty( $this->data->authentication ) || empty( $this->data->authentication ) ) { 100 | return array(); 101 | } 102 | 103 | return (array) $this->data->authentication; 104 | } 105 | 106 | /** 107 | * Does the site support an authentication method? 108 | * 109 | * @param string $method Authentication method to check. 110 | * @return bool True if supported by the site, false otherwise. 111 | */ 112 | public function supportsAuthentication( $method ) { 113 | return array_key_exists( $method, $this->getSupportedAuthentication() ); 114 | } 115 | 116 | /** 117 | * Get method-specific data for the given authentication method. 118 | * 119 | * @param string $method Authentication method to get data for. 120 | * @return mixed Method-specific data if available, null if not supported. 121 | */ 122 | public function getAuthenticationData( $method ) { 123 | if ( ! $this->supportsAuthentication( $method ) ) { 124 | return null; 125 | } 126 | 127 | $authentication = $this->getSupportedAuthentication(); 128 | return $authentication[ $method ]; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /lib/namespace.php: -------------------------------------------------------------------------------- 1 | throw_for_status(); 38 | 39 | $links = $response->headers->getValues( 'Link' ); 40 | 41 | // Find the correct link by relation 42 | foreach ( $links as $link ) { 43 | $attrs = parse_link_header( $link ); 44 | 45 | if ( empty( $attrs ) || empty( $attrs['rel'] ) ) { 46 | continue; 47 | } 48 | switch ( $attrs['rel'] ) { 49 | case 'https://api.w.org/': 50 | break; 51 | 52 | case 'https://github.com/WP-API/WP-API': 53 | // Only allow this if legacy mode is on. 54 | if ( $legacy ) { 55 | break; 56 | } 57 | 58 | // Fall-through. 59 | default: 60 | continue 2; 61 | } 62 | 63 | return $attrs['href']; 64 | } 65 | 66 | return null; 67 | } 68 | 69 | /** 70 | * Parse a Link header into attributes. 71 | * 72 | * @param string $link Link header from the response. 73 | * @return array Map of attribute key => attribute value, with link href in `href` key. 74 | */ 75 | function parse_link_header( $link ) { 76 | $parts = explode( ';', $link ); 77 | $attrs = array( 78 | 'href' => trim( array_shift( $parts ), '<>' ), 79 | ); 80 | 81 | foreach ( $parts as $part ) { 82 | if ( ! strpos( $part, '=' ) ) { 83 | continue; 84 | } 85 | 86 | list( $key, $value ) = explode( '=', $part, 2 ); 87 | $key = trim( $key ); 88 | $value = trim( $value, '" ' ); 89 | $attrs[ $key ] = $value; 90 | } 91 | 92 | return $attrs; 93 | } 94 | 95 | /** 96 | * Get the index information from a site. 97 | * 98 | * @param string $url URL for the API index. 99 | * @return Site Data from the index for the site. 100 | */ 101 | function get_index_information( $url ) { 102 | $response = Requests::get( $url ); 103 | $response->throw_for_status(); 104 | 105 | $index = json_decode( $response->body ); 106 | if ( empty( $index ) && json_last_error() !== JSON_ERROR_NONE ) { 107 | throw new Exception( json_last_error_msg(), json_last_error() ); 108 | } 109 | 110 | return new Site( $index, $url ); 111 | } 112 | -------------------------------------------------------------------------------- /www/details.php: -------------------------------------------------------------------------------- 1 | 8 |
9 | 10 | 11 |

getName() ) ?>

12 | 13 | getDescription() ): ?> 14 |

Site description: getDescription() ) ?>

15 | 16 |

(No site description found.)

17 | 18 | 19 |

Site URL: getURL() ) ?>

20 |

API Index: getIndexURL() ) ?>

21 | 22 |

Supported Namespaces

23 | 28 | 29 |

Supported Authentication Methods

30 | 35 | -------------------------------------------------------------------------------- /www/error.php: -------------------------------------------------------------------------------- 1 |
2 |

Whoops, an error occurred!

3 |

4 |
5 | -------------------------------------------------------------------------------- /www/index.php: -------------------------------------------------------------------------------- 1 | $error, 41 | ))); 42 | return; 43 | } 44 | } catch ( Exception $e ) { 45 | $error = sprintf( 46 | 'Error occurred while trying to discover: %s (%d)', 47 | $e->getMessage(), 48 | $e->getCode() 49 | ); 50 | $output_page( $load_template( 'error', array( 51 | 'error' => $error, 52 | ))); 53 | return; 54 | } 55 | 56 | $output_page( $load_template( 'details', array( 57 | 'site' => $site, 58 | 'uri' => $uri, 59 | ))); 60 | -------------------------------------------------------------------------------- /www/layout.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <?php echo htmlspecialchars( $title ) ?> 5 | 6 | 7 | 8 |
9 |

WordPress API Discovery

10 |
11 |

12 | 17 | 22 |

23 |
24 | 25 |
26 | 27 | -------------------------------------------------------------------------------- /www/style.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Quattrocento:400,700|Fanwood+Text|Fira+Mono); 2 | 3 | 4 | * { 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | text-rendering: optimizeLegibility; 10 | 11 | color: #111; 12 | 13 | font-size: 20px; 14 | line-height: 1.8; 15 | font-family: 'Fanwood Text', Times, Georgia, serif; 16 | font-variant-ligatures: common-ligatures; 17 | font-feature-settings: "liga", "clig"; 18 | -moz-font-feature-settings: "liga", "clig"; 19 | -ms-font-feature-settings: "liga", "clig"; 20 | -webkit-font-feature-settings: "liga", "clig"; 21 | } 22 | 23 | html, body { 24 | margin: 0; 25 | padding: 0; 26 | min-width:320px; 27 | } 28 | 29 | .container { 30 | max-width: 600px; 31 | padding: 1em 0 2.33em; 32 | margin: 0 auto; 33 | position: relative; 34 | overflow: visible; 35 | } 36 | 37 | h1, h2, h3, h4, h5, h6 { 38 | font-family: 'Quattrocento', Times, Georgia, serif; 39 | font-weight: 400; 40 | margin: 0.1em 0 0.2em; 41 | } 42 | 43 | h2 { 44 | font-size: 22px; 45 | margin-bottom: 1.15em; 46 | } 47 | 48 | h3 { 49 | border-bottom: 1px solid #ddd; 50 | margin-top: 2em; 51 | } 52 | 53 | ul, pre, p, blockquote { 54 | margin-top: 0; 55 | margin-bottom: 1em; 56 | } 57 | 58 | ul, li { 59 | // margin:0; 60 | // padding:0; 61 | } 62 | 63 | code, pre { 64 | font-family: 'Fira Mono', monospace; 65 | font-size: 0.9em; 66 | } 67 | 68 | a { 69 | /* http://clrs.cc/ - blue */ 70 | color: #0074D9; 71 | text-decoration: none; 72 | } 73 | 74 | .uri-input { 75 | display: block; 76 | width: 100%; 77 | 78 | font-size: inherit; 79 | font-family: 'Fira Mono', monospace; 80 | padding: 0.2em 0.5em; 81 | } 82 | 83 | .check-legacy { 84 | font-size: 0.9rem; 85 | } 86 | 87 | .warn { 88 | /* http://clrs.cc/ - red */ 89 | background: rgba( 255, 65, 54, 0.4 ); 90 | padding: 1em; 91 | } 92 | --------------------------------------------------------------------------------