├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── composer.json ├── coverage ├── MentionClient.php.html ├── clover.xml ├── css │ ├── bootstrap.min.css │ ├── nv.d3.min.css │ └── style.css ├── dashboard.html ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── index.html └── js │ ├── bootstrap.min.js │ ├── d3.min.js │ ├── holder.min.js │ ├── html5shiv.min.js │ ├── jquery.min.js │ ├── nv.d3.min.js │ └── respond.min.js ├── example.php ├── phpunit ├── phpunit.xml ├── src └── IndieWeb │ ├── MentionClient.php │ └── MentionClientTest.php └── tests ├── DiscoverTest.php ├── FindOutgoingLinksTest.php ├── LinkHeaderParserTest.php ├── SendMentionsTest.php ├── SendPingbackTest.php ├── SendWebmentionTest.php ├── TagParserTest.php ├── bootstrap.php └── data ├── 404.response.txt ├── pingback-endpoint.example ├── 404-response ├── empty-body ├── invalid-body ├── invalid-request ├── invalid-xmlrpc └── valid-response ├── pingback-target.example ├── has-erroring-endpoint.html ├── has-valid-endpoint.html └── no-endpoint.html ├── source.example.com ├── mixed-success-links.html ├── no-links.html ├── send-to-h-entry-links.html ├── target-has-both.html └── two-valid-links.html ├── target.example.com ├── body-a.html ├── body-link-org.html ├── body-link-org2.html ├── body-link.html ├── document-order-1.html ├── document-order-2.html ├── empty-string.html ├── false-endpoint-in-comment.html ├── header.html ├── only-pingback.html ├── pingback-failed.html ├── redirect.html ├── relative │ ├── after-redirect.html │ └── path-relative-endpoint.html ├── webmention-created.html ├── webmention-failed.html ├── webmention-only-failed.html ├── webmention-rocks-test-1.html ├── webmention-rocks-test-2.html ├── webmention-rocks-test-3.html ├── webmention-rocks-test-4.html ├── webmention-rocks-test-5.html ├── webmention-rocks-test-6.html ├── webmention-rocks-test-7.html └── webmention-rocks-test-8.html ├── webmention-endpoint.example ├── 404-response ├── 500-response ├── created-response ├── invalid-request └── queued-response └── webmention-target.example ├── has-erroring-endpoint.html ├── has-valid-endpoint.html └── no-endpoint.html /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | composer.lock 3 | .DS_Store 4 | /vendor/ 5 | .idea/* 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.6 4 | - 7.2 5 | - 7.3 6 | - 7.4 7 | before_script: composer install 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | By contributing to this project, you agree to irrevocably release your contributions under the same licenses as this project. See README.md for more details. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Webmention Client 2 | ================= 3 | 4 | Client library for sending [webmention](http://indiewebcamp.com/webmention) and [pingback](http://indiewebcamp.com/pingback) notifications. 5 | 6 | [![Build Status](https://travis-ci.org/indieweb/mention-client-php.png?branch=master)](http://travis-ci.org/indieweb/mention-client-php) 7 | [![Packagist](https://img.shields.io/packagist/v/indieweb/mention-client.svg)](https://packagist.org/packages/indieweb/mention-client) 8 | 9 | Usage 10 | ----- 11 | 12 | ### Basic Usage 13 | 14 | Given one of your source URLs, this function will find links on the page, 15 | discover the webmention and pingback endpoints for each, and send mentions for any 16 | it discovers. 17 | 18 | ```php 19 | sendMentions($sourceURL); 22 | 23 | echo "Sent $sent mentions\n"; 24 | ?> 25 | ``` 26 | 27 | The library will fetch the source URL, parse it, and look for the first h-entry, 28 | h-card or h-event. It will then attempt to send webmentions (and pingbacks) to 29 | all URLs found in the object, as either properties or inside the "content" HTML. 30 | 31 | The library attempts to find the endpoints by doing a HEAD request to the target URL 32 | and looking at the headers, and if none are found, then it makes a GET request 33 | and searches the body of the page for the rel values. 34 | 35 | After finding either pingback or webmention endpoints, the request is sent to each. 36 | 37 | Alternatively, you can pass in HTML as the second parameter and the library will 38 | look for ALL absolute links in the HTML instead of fetching your post contents and 39 | looking for the microformats object. 40 | 41 | ```php 42 | sendMentions($sourceURL, $sourceHTML); 45 | 46 | echo "Sent $sent mentions\n"; 47 | ?> 48 | ``` 49 | 50 | ### Discovering the Webmention endpoint 51 | 52 | Given a target URL, this function will return its webmention endpoint 53 | if found, otherwise it will return false. 54 | 55 | ```php 56 | discoverWebmentionEndpoint($targetURL); 59 | $endpoint = $client->discoverPingbackEndpoint($targetURL); 60 | ?> 61 | ``` 62 | 63 | ### Sending a Webmention 64 | 65 | To send a webmention to a target URL, you can use the function below. This will 66 | first discover the webmention endpoint of the target, and if found, will then 67 | send the webmention payload to it. You can pass an additional parameter to include 68 | other properties in the payload. 69 | 70 | ```php 71 | sendWebmention($sourceURL, $targetURL); 74 | $response = $client->sendWebmention($sourceURL, $targetURL, ['vouch'=>$vouch]); 75 | ?> 76 | ``` 77 | 78 | If no webmention endpoint was found at the target, the function will return false. 79 | See the function below for an example of the response when the webmention is successful. 80 | 81 | You can also check if the endpoint advertises a webmention endpoint before trying 82 | to send one: 83 | 84 | ```php 85 | discoverWebmentionEndpoint($targetURL); 88 | if($supportsWebmention) { 89 | $client->sendWebmention($sourceURL, $targetURL); 90 | } 91 | ?> 92 | ``` 93 | 94 | ### Sending a Pingback 95 | 96 | ```php 97 | sendPingback($sourceURL, $targetURL); 100 | ?> 101 | ``` 102 | 103 | You can also check if the endpoint advertises a pingback endpoint before trying 104 | to send one: 105 | 106 | ```php 107 | discoverPingbackEndpoint($targetURL); 110 | if($supportsPingback) { 111 | $client->sendPingback($sourceURL, $targetURL); 112 | } 113 | ?> 114 | ``` 115 | 116 | ### Sending the Webmention or Pingback directly 117 | 118 | To send the actual webmention or pingback payload, you can use the static functions below. 119 | You can pass additional properties for the webmention request in an array if needed. 120 | 121 | ```php 122 | $vouch]); 125 | ?> 126 | ``` 127 | 128 | The response is an array containing the HTTP status code, HTTP headers, and the response body: 129 | 130 | ```json 131 | { 132 | "code": 202, 133 | "headers": { 134 | "Content-Type: text/plain" 135 | }, 136 | "body": "Webmention is processing" 137 | } 138 | ``` 139 | 140 | You can check if the webmention was accepted by testing if the response code is 200, 201 or 202. 141 | 142 | ```php 143 | 146 | ``` 147 | 148 | The pingback function returns true or false depending on whether the pingback was successfully sent. 149 | 150 | 151 | 152 | ### Finding target URLs in a source document 153 | 154 | If you have a rendered HTML page (or partial HTML page), you can use this function to 155 | return a list of outgoing links found on the page. 156 | 157 | ```php 158 | findOutgoingLinks($html); 161 | ?> 162 | ``` 163 | 164 | Alternately, you can pass a parsed Microformats object to the `findOutgoingLinks` 165 | function and it will search for URLs in any property as well as in the HTML of 166 | any e-content objects. 167 | 168 | ```php 169 | $client = new IndieWeb\MentionClient(); 170 | $parsed = \Mf2\parse($html, $sourceURL); 171 | $urls = $client->findOutgoingLinks($parsed); 172 | ``` 173 | 174 | All links found will be returned an array, with duplicate URLs removed. If no links 175 | are found, it will return an empty array. 176 | 177 | ```json 178 | [ 179 | "http://example.com/1", 180 | "http://example.com/2" 181 | ] 182 | ``` 183 | 184 | ### Custom User Agent 185 | 186 | You can set the user agent that this library uses when making HTTP requests. 187 | 188 | ```php 189 | IndieWeb\MentionClient::setUserAgent('Custom user agent string'); 190 | ``` 191 | 192 | At that point, any HTTP request (GET, HEAD, POST) that this library makes will include the user agent header you've set. 193 | 194 | 195 | ### Debugging 196 | 197 | If you want to collect debugging information so you can see the steps the library 198 | is doing, run `IndieWeb\MentionClient::enableDebug();` before calling any other function. 199 | 200 | 201 | 202 | 203 | About Webmention 204 | ---------------- 205 | 206 | To learn more about Webmention, see [webmention.net](http://webmention.net). 207 | 208 | The [webmention.io](http://webmention.io/) service can also act as a pingback->webmention 209 | proxy which will allow you to accept pingbacks as if they were sent as webmentions. 210 | 211 | 212 | About Pingback 213 | -------------- 214 | 215 | If you want to accept pingbacks on your site, check out [webmention.io](http://webmention.io/#use-it) 216 | which handles accepting the XMLRPC request and exposes the data via an API. 217 | 218 | 219 | License 220 | ------- 221 | 222 | Copyright 2013-2017 by Aaron Parecki and contributors 223 | 224 | Available under the Apache 2.0 and MIT licenses. 225 | 226 | #### Apache 2.0 227 | 228 | Licensed under the Apache License, Version 2.0 (the "License"); 229 | you may not use this file except in compliance with the License. 230 | You may obtain a copy of the License at 231 | 232 | http://www.apache.org/licenses/LICENSE-2.0 233 | 234 | Unless required by applicable law or agreed to in writing, software 235 | distributed under the License is distributed on an "AS IS" BASIS, 236 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 237 | See the License for the specific language governing permissions and 238 | limitations under the License. 239 | 240 | #### MIT 241 | 242 | Permission is hereby granted, free of charge, to any person obtaining a copy 243 | of this software and associated documentation files (the "Software"), to deal 244 | in the Software without restriction, including without limitation the rights 245 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 246 | copies of the Software, and to permit persons to whom the Software is 247 | furnished to do so, subject to the following conditions: 248 | 249 | The above copyright notice and this permission notice shall be included in all 250 | copies or substantial portions of the Software. 251 | 252 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 253 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 254 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 255 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 256 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 257 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 258 | SOFTWARE. 259 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "indieweb/mention-client", 3 | "description": "Client library for sending webmention and pingback notifications", 4 | "type": "library", 5 | "license": "Apache-2.0", 6 | "homepage": "https://github.com/indieweb/mention-client-php", 7 | "authors": [ 8 | { 9 | "name": "Aaron Parecki", 10 | "email": "aaron@parecki.com", 11 | "homepage": "http://aaronparecki.com/" 12 | } 13 | ], 14 | "autoload": { 15 | "psr-0": { 16 | "IndieWeb": "src/" 17 | } 18 | }, 19 | "minimum-stability": "dev", 20 | "require": { 21 | "php": ">=5.6", 22 | "mf2/mf2": ">=0.2" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "^6" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /coverage/clover.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | -------------------------------------------------------------------------------- /coverage/css/nv.d3.min.css: -------------------------------------------------------------------------------- 1 | .nvd3 .nv-axis{pointer-events:none;opacity:1}.nvd3 .nv-axis path{fill:none;stroke:#000;stroke-opacity:.75;shape-rendering:crispEdges}.nvd3 .nv-axis path.domain{stroke-opacity:.75}.nvd3 .nv-axis.nv-x path.domain{stroke-opacity:0}.nvd3 .nv-axis line{fill:none;stroke:#e5e5e5;shape-rendering:crispEdges}.nvd3 .nv-axis .zero line,.nvd3 .nv-axis line.zero{stroke-opacity:.75}.nvd3 .nv-axis .nv-axisMaxMin text{font-weight:700}.nvd3 .x .nv-axis .nv-axisMaxMin text,.nvd3 .x2 .nv-axis .nv-axisMaxMin text,.nvd3 .x3 .nv-axis .nv-axisMaxMin text{text-anchor:middle}.nvd3 .nv-axis.nv-disabled{opacity:0}.nvd3 .nv-bars rect{fill-opacity:.75;transition:fill-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear}.nvd3 .nv-bars rect.hover{fill-opacity:1}.nvd3 .nv-bars .hover rect{fill:#add8e6}.nvd3 .nv-bars text{fill:rgba(0,0,0,0)}.nvd3 .nv-bars .hover text{fill:rgba(0,0,0,1)}.nvd3 .nv-multibar .nv-groups rect,.nvd3 .nv-multibarHorizontal .nv-groups rect,.nvd3 .nv-discretebar .nv-groups rect{stroke-opacity:0;transition:fill-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear}.nvd3 .nv-multibar .nv-groups rect:hover,.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,.nvd3 .nv-candlestickBar .nv-ticks rect:hover,.nvd3 .nv-discretebar .nv-groups rect:hover{fill-opacity:1}.nvd3 .nv-discretebar .nv-groups text,.nvd3 .nv-multibarHorizontal .nv-groups text{font-weight:700;fill:rgba(0,0,0,1);stroke:rgba(0,0,0,0)}.nvd3 .nv-boxplot circle{fill-opacity:.5}.nvd3 .nv-boxplot circle:hover{fill-opacity:1}.nvd3 .nv-boxplot rect:hover{fill-opacity:1}.nvd3 line.nv-boxplot-median{stroke:#000}.nv-boxplot-tick:hover{stroke-width:2.5px}.nvd3.nv-bullet{font:10px sans-serif}.nvd3.nv-bullet .nv-measure{fill-opacity:.8}.nvd3.nv-bullet .nv-measure:hover{fill-opacity:1}.nvd3.nv-bullet .nv-marker{stroke:#000;stroke-width:2px}.nvd3.nv-bullet .nv-markerTriangle{stroke:#000;fill:#fff;stroke-width:1.5px}.nvd3.nv-bullet .nv-tick line{stroke:#666;stroke-width:.5px}.nvd3.nv-bullet .nv-range.nv-s0{fill:#eee}.nvd3.nv-bullet .nv-range.nv-s1{fill:#ddd}.nvd3.nv-bullet .nv-range.nv-s2{fill:#ccc}.nvd3.nv-bullet .nv-title{font-size:14px;font-weight:700}.nvd3.nv-bullet .nv-subtitle{fill:#999}.nvd3.nv-bullet .nv-range{fill:#bababa;fill-opacity:.4}.nvd3.nv-bullet .nv-range:hover{fill-opacity:.7}.nvd3.nv-candlestickBar .nv-ticks .nv-tick{stroke-width:1px}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.hover{stroke-width:2px}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.positive rect{stroke:#2ca02c;fill:#2ca02c}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.negative rect{stroke:#d62728;fill:#d62728}.with-transitions .nv-candlestickBar .nv-ticks .nv-tick{transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-moz-transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-webkit-transition:stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-candlestickBar .nv-ticks line{stroke:#333}.nvd3 .nv-legend .nv-disabled rect{}.nvd3 .nv-check-box .nv-box{fill-opacity:0;stroke-width:2}.nvd3 .nv-check-box .nv-check{fill-opacity:0;stroke-width:4}.nvd3 .nv-series.nv-disabled .nv-check-box .nv-check{fill-opacity:0;stroke-opacity:0}.nvd3 .nv-controlsWrap .nv-legend .nv-check-box .nv-check{opacity:0}.nvd3.nv-linePlusBar .nv-bar rect{fill-opacity:.75}.nvd3.nv-linePlusBar .nv-bar rect:hover{fill-opacity:1}.nvd3 .nv-groups path.nv-line{fill:none}.nvd3 .nv-groups path.nv-area{stroke:none}.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point{fill-opacity:0;stroke-opacity:0}.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point{fill-opacity:.5!important;stroke-opacity:.5!important}.with-transitions .nvd3 .nv-groups .nv-point{transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-moz-transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-webkit-transition:stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-scatter .nv-groups .nv-point.hover,.nvd3 .nv-groups .nv-point.hover{stroke-width:7px;fill-opacity:.95!important;stroke-opacity:.95!important}.nvd3 .nv-point-paths path{stroke:#aaa;stroke-opacity:0;fill:#eee;fill-opacity:0}.nvd3 .nv-indexLine{cursor:ew-resize}svg.nvd3-svg{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-ms-user-select:none;-moz-user-select:none;user-select:none;display:block;width:100%;height:100%}.nvtooltip.with-3d-shadow,.with-3d-shadow .nvtooltip{-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nvd3 text{font:400 12px Arial}.nvd3 .title{font:700 14px Arial}.nvd3 .nv-background{fill:#fff;fill-opacity:0}.nvd3.nv-noData{font-size:18px;font-weight:700}.nv-brush .extent{fill-opacity:.125;shape-rendering:crispEdges}.nv-brush .resize path{fill:#eee;stroke:#666}.nvd3 .nv-legend .nv-series{cursor:pointer}.nvd3 .nv-legend .nv-disabled circle{fill-opacity:0}.nvd3 .nv-brush .extent{fill-opacity:0!important}.nvd3 .nv-brushBackground rect{stroke:#000;stroke-width:.4;fill:#fff;fill-opacity:.7}.nvd3.nv-ohlcBar .nv-ticks .nv-tick{stroke-width:1px}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover{stroke-width:2px}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive{stroke:#2ca02c}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative{stroke:#d62728}.nvd3 .background path{fill:none;stroke:#EEE;stroke-opacity:.4;shape-rendering:crispEdges}.nvd3 .foreground path{fill:none;stroke-opacity:.7}.nvd3 .nv-parallelCoordinates-brush .extent{fill:#fff;fill-opacity:.6;stroke:gray;shape-rendering:crispEdges}.nvd3 .nv-parallelCoordinates .hover{fill-opacity:1;stroke-width:3px}.nvd3 .missingValuesline line{fill:none;stroke:#000;stroke-width:1;stroke-opacity:1;stroke-dasharray:5,5}.nvd3.nv-pie path{stroke-opacity:0;transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-pie .nv-pie-title{font-size:24px;fill:rgba(19,196,249,.59)}.nvd3.nv-pie .nv-slice text{stroke:#000;stroke-width:0}.nvd3.nv-pie path{stroke:#fff;stroke-width:1px;stroke-opacity:1}.nvd3.nv-pie .hover path{fill-opacity:.7}.nvd3.nv-pie .nv-label{pointer-events:none}.nvd3.nv-pie .nv-label rect{fill-opacity:0;stroke-opacity:0}.nvd3 .nv-groups .nv-point.hover{stroke-width:20px;stroke-opacity:.5}.nvd3 .nv-scatter .nv-point.hover{fill-opacity:1}.nv-noninteractive{pointer-events:none}.nv-distx,.nv-disty{pointer-events:none}.nvd3.nv-sparkline path{fill:none}.nvd3.nv-sparklineplus g.nv-hoverValue{pointer-events:none}.nvd3.nv-sparklineplus .nv-hoverValue line{stroke:#333;stroke-width:1.5px}.nvd3.nv-sparklineplus,.nvd3.nv-sparklineplus g{pointer-events:all}.nvd3 .nv-hoverArea{fill-opacity:0;stroke-opacity:0}.nvd3.nv-sparklineplus .nv-xValue,.nvd3.nv-sparklineplus .nv-yValue{stroke-width:0;font-size:.9em;font-weight:400}.nvd3.nv-sparklineplus .nv-yValue{stroke:#f66}.nvd3.nv-sparklineplus .nv-maxValue{stroke:#2ca02c;fill:#2ca02c}.nvd3.nv-sparklineplus .nv-minValue{stroke:#d62728;fill:#d62728}.nvd3.nv-sparklineplus .nv-currentValue{font-weight:700;font-size:1.1em}.nvd3.nv-stackedarea path.nv-area{fill-opacity:.7;stroke-opacity:0;transition:fill-opacity 250ms linear,stroke-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear,stroke-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-stackedarea path.nv-area.hover{fill-opacity:.9}.nvd3.nv-stackedarea .nv-groups .nv-point{stroke-opacity:0;fill-opacity:0}.nvtooltip{position:absolute;background-color:rgba(255,255,255,1);color:rgba(0,0,0,1);padding:1px;border:1px solid rgba(0,0,0,.2);z-index:10000;display:block;font-family:Arial;font-size:13px;text-align:left;pointer-events:none;white-space:nowrap;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.nvtooltip{background:rgba(255,255,255,.8);border:1px solid rgba(0,0,0,.5);border-radius:4px}.nvtooltip.with-transitions,.with-transitions .nvtooltip{transition:opacity 50ms linear;-moz-transition:opacity 50ms linear;-webkit-transition:opacity 50ms linear;transition-delay:200ms;-moz-transition-delay:200ms;-webkit-transition-delay:200ms}.nvtooltip.x-nvtooltip,.nvtooltip.y-nvtooltip{padding:8px}.nvtooltip h3{margin:0;padding:4px 14px;line-height:18px;font-weight:400;background-color:rgba(247,247,247,.75);color:rgba(0,0,0,1);text-align:center;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.nvtooltip p{margin:0;padding:5px 14px;text-align:center}.nvtooltip span{display:inline-block;margin:2px 0}.nvtooltip table{margin:6px;border-spacing:0}.nvtooltip table td{padding:2px 9px 2px 0;vertical-align:middle}.nvtooltip table td.key{font-weight:400}.nvtooltip table td.value{text-align:right;font-weight:700}.nvtooltip table tr.highlight td{padding:1px 9px 1px 0;border-bottom-style:solid;border-bottom-width:1px;border-top-style:solid;border-top-width:1px}.nvtooltip table td.legend-color-guide div{width:8px;height:8px;vertical-align:middle}.nvtooltip table td.legend-color-guide div{width:12px;height:12px;border:1px solid #999}.nvtooltip .footer{padding:3px;text-align:center}.nvtooltip-pending-removal{pointer-events:none;display:none}.nvd3 .nv-interactiveGuideLine{pointer-events:none}.nvd3 line.nv-guideline{stroke:#ccc} -------------------------------------------------------------------------------- /coverage/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 10px; 3 | } 4 | 5 | .popover { 6 | max-width: none; 7 | } 8 | 9 | .glyphicon { 10 | margin-right:.25em; 11 | } 12 | 13 | .table-bordered>thead>tr>td { 14 | border-bottom-width: 1px; 15 | } 16 | 17 | .table tbody>tr>td, .table thead>tr>td { 18 | padding-top: 3px; 19 | padding-bottom: 3px; 20 | } 21 | 22 | .table-condensed tbody>tr>td { 23 | padding-top: 0; 24 | padding-bottom: 0; 25 | } 26 | 27 | .table .progress { 28 | margin-bottom: inherit; 29 | } 30 | 31 | .table-borderless th, .table-borderless td { 32 | border: 0 !important; 33 | } 34 | 35 | .table tbody tr.covered-by-large-tests, li.covered-by-large-tests, tr.success, td.success, li.success, span.success { 36 | background-color: #dff0d8; 37 | } 38 | 39 | .table tbody tr.covered-by-medium-tests, li.covered-by-medium-tests { 40 | background-color: #c3e3b5; 41 | } 42 | 43 | .table tbody tr.covered-by-small-tests, li.covered-by-small-tests { 44 | background-color: #99cb84; 45 | } 46 | 47 | .table tbody tr.danger, .table tbody td.danger, li.danger, span.danger { 48 | background-color: #f2dede; 49 | } 50 | 51 | .table tbody td.warning, li.warning, span.warning { 52 | background-color: #fcf8e3; 53 | } 54 | 55 | .table tbody td.info { 56 | background-color: #d9edf7; 57 | } 58 | 59 | td.big { 60 | width: 117px; 61 | } 62 | 63 | td.small { 64 | } 65 | 66 | td.codeLine { 67 | font-family: monospace; 68 | white-space: pre; 69 | } 70 | 71 | td span.comment { 72 | color: #888a85; 73 | } 74 | 75 | td span.default { 76 | color: #2e3436; 77 | } 78 | 79 | td span.html { 80 | color: #888a85; 81 | } 82 | 83 | td span.keyword { 84 | color: #2e3436; 85 | font-weight: bold; 86 | } 87 | 88 | pre span.string { 89 | color: #2e3436; 90 | } 91 | 92 | span.success, span.warning, span.danger { 93 | margin-right: 2px; 94 | padding-left: 10px; 95 | padding-right: 10px; 96 | text-align: center; 97 | } 98 | 99 | #classCoverageDistribution, #classComplexity { 100 | height: 200px; 101 | width: 475px; 102 | } 103 | 104 | #toplink { 105 | position: fixed; 106 | left: 5px; 107 | bottom: 5px; 108 | outline: 0; 109 | } 110 | 111 | svg text { 112 | font-family: "Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif; 113 | font-size: 11px; 114 | color: #666; 115 | fill: #666; 116 | } 117 | 118 | .scrollbox { 119 | height:245px; 120 | overflow-x:hidden; 121 | overflow-y:scroll; 122 | } 123 | -------------------------------------------------------------------------------- /coverage/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dashboard for /Users/aaronpk/Code/mention-client-php/src/IndieWeb 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 |
17 |
18 |
19 |
20 | 25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |

Classes

33 |
34 |
35 |
36 |
37 |

Coverage Distribution

38 |
39 | 40 |
41 |
42 |
43 |

Complexity

44 |
45 | 46 |
47 |
48 |
49 |
50 |
51 |

Insufficient Coverage

52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
ClassCoverage
64 |
65 |
66 |
67 |

Project Risks

68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
ClassCRAP
80 |
81 |
82 |
83 |
84 |
85 |

Methods

86 |
87 |
88 |
89 |
90 |

Coverage Distribution

91 |
92 | 93 |
94 |
95 |
96 |

Complexity

97 |
98 | 99 |
100 |
101 |
102 |
103 |
104 |

Insufficient Coverage

105 |
106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 |
MethodCoverage
117 |
118 |
119 |
120 |

Project Risks

121 |
122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 |
MethodCRAP
133 |
134 |
135 |
136 | 142 |
143 | 144 | 145 | 146 | 147 | 148 | 285 | 286 | 287 | -------------------------------------------------------------------------------- /coverage/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indieweb/mention-client-php/6807f4b1b96df7f47ea425eebfc7c032768652cf/coverage/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /coverage/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indieweb/mention-client-php/6807f4b1b96df7f47ea425eebfc7c032768652cf/coverage/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /coverage/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indieweb/mention-client-php/6807f4b1b96df7f47ea425eebfc7c032768652cf/coverage/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /coverage/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indieweb/mention-client-php/6807f4b1b96df7f47ea425eebfc7c032768652cf/coverage/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /coverage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Code Coverage for /Users/aaronpk/Code/mention-client-php/src/IndieWeb 6 | 7 | 8 | 9 | 13 | 14 | 15 |
16 |
17 |
18 |
19 | 24 |
25 |
26 |
27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 51 | 52 | 53 | 59 | 60 | 61 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 81 | 87 | 88 | 89 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
 
Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
46 |
47 | 100.00% covered (success) 48 |
49 |
50 |
100.00%
229 / 229
54 |
55 | 100.00% covered (success) 56 |
57 |
58 |
100.00%
18 / 18
62 |
63 | 100.00% covered (success) 64 |
65 |
66 |
100.00%
1 / 1
MentionClient.php
74 |
75 | 100.00% covered (success) 76 |
77 |
78 |
100.00%
229 / 229
82 |
83 | 100.00% covered (success) 84 |
85 |
86 |
100.00%
18 / 18
90 |
91 | 100.00% covered (success) 92 |
93 |
94 |
100.00%
1 / 1
102 | 114 |
115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /coverage/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.4 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.4",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.4",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.4",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.4",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.4",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(this.options.viewport.selector||this.options.viewport),this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c&&c.$tip&&c.$tip.is(":visible")?void(c.hoverState="in"):(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.options.container?a(this.options.container):this.$element.parent(),p=this.getPosition(o);h="bottom"==h&&k.bottom+m>p.bottom?"top":"top"==h&&k.top-mp.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.width&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type)})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.4",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.4",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.4",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=a(document.body).height();"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); -------------------------------------------------------------------------------- /coverage/js/holder.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | Holder - client side image placeholders 4 | Version 2.7.1+6hydf 5 | © 2015 Ivan Malopinsky - http://imsky.co 6 | 7 | Site: http://holderjs.com 8 | Issues: https://github.com/imsky/holder/issues 9 | License: http://opensource.org/licenses/MIT 10 | 11 | */ 12 | !function(a){if(a.document){var b=a.document;b.querySelectorAll||(b.querySelectorAll=function(c){var d,e=b.createElement("style"),f=[];for(b.documentElement.firstChild.appendChild(e),b._qsa=[],e.styleSheet.cssText=c+"{x-qsa:expression(document._qsa && document._qsa.push(this))}",a.scrollBy(0,0),e.parentNode.removeChild(e);b._qsa.length;)d=b._qsa.shift(),d.style.removeAttribute("x-qsa"),f.push(d);return b._qsa=null,f}),b.querySelector||(b.querySelector=function(a){var c=b.querySelectorAll(a);return c.length?c[0]:null}),b.getElementsByClassName||(b.getElementsByClassName=function(a){return a=String(a).replace(/^|\s+/g,"."),b.querySelectorAll(a)}),Object.keys||(Object.keys=function(a){if(a!==Object(a))throw TypeError("Object.keys called on non-object");var b,c=[];for(b in a)Object.prototype.hasOwnProperty.call(a,b)&&c.push(b);return c}),function(a){var b="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";a.atob=a.atob||function(a){a=String(a);var c,d=0,e=[],f=0,g=0;if(a=a.replace(/\s/g,""),a.length%4===0&&(a=a.replace(/=+$/,"")),a.length%4===1)throw Error("InvalidCharacterError");if(/[^+/0-9A-Za-z]/.test(a))throw Error("InvalidCharacterError");for(;d>16&255)),e.push(String.fromCharCode(f>>8&255)),e.push(String.fromCharCode(255&f)),g=0,f=0),d+=1;return 12===g?(f>>=4,e.push(String.fromCharCode(255&f))):18===g&&(f>>=2,e.push(String.fromCharCode(f>>8&255)),e.push(String.fromCharCode(255&f))),e.join("")},a.btoa=a.btoa||function(a){a=String(a);var c,d,e,f,g,h,i,j=0,k=[];if(/[^\x00-\xFF]/.test(a))throw Error("InvalidCharacterError");for(;j>2,g=(3&c)<<4|d>>4,h=(15&d)<<2|e>>6,i=63&e,j===a.length+2?(h=64,i=64):j===a.length+1&&(i=64),k.push(b.charAt(f),b.charAt(g),b.charAt(h),b.charAt(i));return k.join("")}}(a),Object.prototype.hasOwnProperty||(Object.prototype.hasOwnProperty=function(a){var b=this.__proto__||this.constructor.prototype;return a in this&&(!(a in b)||b[a]!==this[a])}),function(){if("performance"in a==!1&&(a.performance={}),Date.now=Date.now||function(){return(new Date).getTime()},"now"in a.performance==!1){var b=Date.now();performance.timing&&performance.timing.navigationStart&&(b=performance.timing.navigationStart),a.performance.now=function(){return Date.now()-b}}}(),a.requestAnimationFrame||(a.webkitRequestAnimationFrame?!function(a){a.requestAnimationFrame=function(b){return webkitRequestAnimationFrame(function(){b(a.performance.now())})},a.cancelAnimationFrame=webkitCancelAnimationFrame}(a):a.mozRequestAnimationFrame?!function(a){a.requestAnimationFrame=function(b){return mozRequestAnimationFrame(function(){b(a.performance.now())})},a.cancelAnimationFrame=mozCancelAnimationFrame}(a):!function(a){a.requestAnimationFrame=function(b){return a.setTimeout(b,1e3/60)},a.cancelAnimationFrame=a.clearTimeout}(a))}}(this),function(a,b){"object"==typeof exports&&"object"==typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):"object"==typeof exports?exports.Holder=b():a.Holder=b()}(this,function(){return function(a){function b(d){if(c[d])return c[d].exports;var e=c[d]={exports:{},id:d,loaded:!1};return a[d].call(e.exports,e,e.exports,b),e.loaded=!0,e.exports}var c={};return b.m=a,b.c=c,b.p="",b(0)}([function(a,b,c){(function(b){function d(a,b,c,d){var f=e(c.substr(c.lastIndexOf(a.domain)),a);f&&h({mode:null,el:d,flags:f,engineSettings:b})}function e(a,b){var c={theme:B(J.settings.themes.gray,null),stylesheets:b.stylesheets,instanceOptions:b};return a.match(/([\d]+p?)x([\d]+p?)(?:\?|$)/)?f(a,c):g(a,c)}function f(a,b){var c=a.split("?"),d=c[0].split("/");b.holderURL=a;var e=d[1],f=e.match(/([\d]+p?)x([\d]+p?)/);if(!f)return!1;if(b.fluid=-1!==e.indexOf("p"),b.dimensions={width:f[1].replace("p","%"),height:f[2].replace("p","%")},2===c.length){var g=A.parse(c[1]);if(g.bg&&(b.theme.background=(-1===g.bg.indexOf("#")?"#":"")+g.bg),g.fg&&(b.theme.foreground=(-1===g.fg.indexOf("#")?"#":"")+g.fg),g.theme&&b.instanceOptions.themes.hasOwnProperty(g.theme)&&(b.theme=B(b.instanceOptions.themes[g.theme],null)),g.text&&(b.text=g.text),g.textmode&&(b.textmode=g.textmode),g.size&&(b.size=g.size),g.font&&(b.font=g.font),g.align&&(b.align=g.align),b.nowrap=z.truthy(g.nowrap),b.auto=z.truthy(g.auto),z.truthy(g.random)){J.vars.cache.themeKeys=J.vars.cache.themeKeys||Object.keys(b.instanceOptions.themes);var h=J.vars.cache.themeKeys[0|Math.random()*J.vars.cache.themeKeys.length];b.theme=B(b.instanceOptions.themes[h],null)}}return b}function g(a,b){var c=!1,d=String.fromCharCode(11),e=a.replace(/([^\\])\//g,"$1"+d).split(d),f=/%[0-9a-f]{2}/gi,g=b.instanceOptions;b.holderURL=[];for(var h=e.length,i=0;h>i;i++){var j=e[i];if(j.match(f))try{j=decodeURIComponent(j)}catch(k){j=e[i]}var l=!1;if(J.flags.dimensions.match(j))c=!0,b.dimensions=J.flags.dimensions.output(j),l=!0;else if(J.flags.fluid.match(j))c=!0,b.dimensions=J.flags.fluid.output(j),b.fluid=!0,l=!0;else if(J.flags.textmode.match(j))b.textmode=J.flags.textmode.output(j),l=!0;else if(J.flags.colors.match(j)){var m=J.flags.colors.output(j);b.theme=B(b.theme,m),l=!0}else if(g.themes[j])g.themes.hasOwnProperty(j)&&(b.theme=B(g.themes[j],null)),l=!0;else if(J.flags.font.match(j))b.font=J.flags.font.output(j),l=!0;else if(J.flags.auto.match(j))b.auto=!0,l=!0;else if(J.flags.text.match(j))b.text=J.flags.text.output(j),l=!0;else if(J.flags.size.match(j))b.size=J.flags.size.output(j),l=!0;else if(J.flags.random.match(j)){null==J.vars.cache.themeKeys&&(J.vars.cache.themeKeys=Object.keys(g.themes));var n=J.vars.cache.themeKeys[0|Math.random()*J.vars.cache.themeKeys.length];b.theme=B(g.themes[n],null),l=!0}l&&b.holderURL.push(j)}return b.holderURL.unshift(g.domain),b.holderURL=b.holderURL.join("/"),c?b:!1}function h(a){var b=a.mode,c=a.el,d=a.flags,e=a.engineSettings,f=d.dimensions,g=d.theme,h=f.width+"x"+f.height;if(b=null==b?d.fluid?"fluid":"image":b,null!=d.text&&(g.text=d.text,"object"===c.nodeName.toLowerCase())){for(var j=g.text.split("\\n"),k=0;k1){var n,o=0,p=0,q=0;j=new e.Group("line"+q),("left"===a.align||"right"===a.align)&&(m=a.width*(1-2*(1-J.setup.lineWrapRatio)));for(var r=0;r=m||t===!0)&&(b(g,j,o,g.properties.leading),g.add(j),o=0,p+=g.properties.leading,q+=1,j=new e.Group("line"+q),j.y=p),t!==!0&&(i.moveTo(o,0),o+=h.spaceWidth+s.width,j.add(i))}if(b(g,j,o,g.properties.leading),g.add(j),"left"===a.align)g.moveTo(a.width-l,null,null);else if("right"===a.align){for(n in g.children)j=g.children[n],j.moveTo(a.width-j.width,null,null);g.moveTo(0-(a.width-l),null,null)}else{for(n in g.children)j=g.children[n],j.moveTo((g.width-j.width)/2,null,null);g.moveTo((a.width-g.width)/2,null,null)}g.moveTo(null,(a.height-g.height)/2,null),(a.height-g.height)/2<0&&g.moveTo(null,0,null)}else i=new e.Text(a.text),j=new e.Group("line0"),j.add(i),g.add(j),"left"===a.align?g.moveTo(a.width-l,null,null):"right"===a.align?g.moveTo(0-(a.width-l),null,null):g.moveTo((a.width-h.boundingBox.width)/2,null,null),g.moveTo(null,(a.height-h.boundingBox.height)/2,null);return d}function k(a,b,c){var d=parseInt(a,10),e=parseInt(b,10),f=Math.max(d,e),g=Math.min(d,e),h=.8*Math.min(g,f*J.defaults.scale);return Math.round(Math.max(c,h))}function l(a){var b;b=null==a||null==a.nodeType?J.vars.resizableImages:[a];for(var c=0,d=b.length;d>c;c++){var e=b[c];if(e.holderData){var f=e.holderData.flags,g=D(e);if(g){if(!e.holderData.resizeUpdate)continue;if(f.fluid&&f.auto){var h=e.holderData.fluidConfig;switch(h.mode){case"width":g.height=g.width/h.ratio;break;case"height":g.width=g.height*h.ratio}}var j={mode:"image",holderSettings:{dimensions:g,theme:f.theme,flags:f},el:e,engineSettings:e.holderData.engineSettings};"exact"==f.textmode&&(f.exactDimensions=g,j.holderSettings.dimensions=f.dimensions),i(j)}else p(e)}}}function m(a){if(a.holderData){var b=D(a);if(b){var c=a.holderData.flags,d={fluidHeight:"%"==c.dimensions.height.slice(-1),fluidWidth:"%"==c.dimensions.width.slice(-1),mode:null,initialDimensions:b};d.fluidWidth&&!d.fluidHeight?(d.mode="width",d.ratio=d.initialDimensions.width/parseFloat(c.dimensions.height)):!d.fluidWidth&&d.fluidHeight&&(d.mode="height",d.ratio=parseFloat(c.dimensions.width)/d.initialDimensions.height),a.holderData.fluidConfig=d}else p(a)}}function n(){for(var a,c=[],d=Object.keys(J.vars.invisibleImages),e=0,f=d.length;f>e;e++)a=J.vars.invisibleImages[d[e]],D(a)&&"img"==a.nodeName.toLowerCase()&&(c.push(a),delete J.vars.invisibleImages[d[e]]);c.length&&I.run({images:c}),b.requestAnimationFrame(n)}function o(){J.vars.visibilityCheckStarted||(b.requestAnimationFrame(n),J.vars.visibilityCheckStarted=!0)}function p(a){a.holderData.invisibleId||(J.vars.invisibleId+=1,J.vars.invisibleImages["i"+J.vars.invisibleId]=a,a.holderData.invisibleId=J.vars.invisibleId)}function q(a,b){return null==b?document.createElement(a):document.createElementNS(b,a)}function r(a,b){for(var c in b)a.setAttribute(c,b[c])}function s(a,b,c){var d,e;null==a?(a=q("svg",E),d=q("defs",E),e=q("style",E),r(e,{type:"text/css"}),d.appendChild(e),a.appendChild(d)):e=a.querySelector("style"),a.webkitMatchesSelector&&a.setAttribute("xmlns",E);for(var f=0;f=0;h--){var i=g.createProcessingInstruction("xml-stylesheet",'href="'+f[h]+'" rel="stylesheet"');g.insertBefore(i,g.firstChild)}g.removeChild(g.documentElement),e=d.serializeToString(g)}var j=d.serializeToString(a);return j=j.replace(/\&(\#[0-9]{2,}\;)/g,"&$1"),e+j}}function u(){return b.DOMParser?(new DOMParser).parseFromString("","application/xml"):void 0}function v(a){J.vars.debounceTimer||a.call(this),J.vars.debounceTimer&&b.clearTimeout(J.vars.debounceTimer),J.vars.debounceTimer=b.setTimeout(function(){J.vars.debounceTimer=null,a.call(this)},J.setup.debounce)}function w(){v(function(){l(null)})}var x=c(1),y=c(2),z=c(3),A=c(4),B=z.extend,C=z.getNodeArray,D=z.dimensionCheck,E="http://www.w3.org/2000/svg",F=8,G="2.7.1",H="\nCreated with Holder.js "+G+".\nLearn more at http://holderjs.com\n(c) 2012-2015 Ivan Malopinsky - http://imsky.co\n",I={version:G,addTheme:function(a,b){return null!=a&&null!=b&&(J.settings.themes[a]=b),delete J.vars.cache.themeKeys,this},addImage:function(a,b){var c=document.querySelectorAll(b);if(c.length)for(var d=0,e=c.length;e>d;d++){var f=q("img"),g={};g[J.vars.dataAttr]=a,r(f,g),c[d].appendChild(f)}return this},setResizeUpdate:function(a,b){a.holderData&&(a.holderData.resizeUpdate=!!b,a.holderData.resizeUpdate&&l(a))},run:function(a){a=a||{};var c={},f=B(J.settings,a);J.vars.preempted=!0,J.vars.dataAttr=f.dataAttr||J.vars.dataAttr,c.renderer=f.renderer?f.renderer:J.setup.renderer,-1===J.setup.renderers.join(",").indexOf(c.renderer)&&(c.renderer=J.setup.supportsSVG?"svg":J.setup.supportsCanvas?"canvas":"html");var g=C(f.images),i=C(f.bgnodes),j=C(f.stylenodes),k=C(f.objects);c.stylesheets=[],c.svgXMLStylesheet=!0,c.noFontFallback=f.noFontFallback?f.noFontFallback:!1;for(var l=0;l1){c.nodeValue="";for(var u=0;u=0?b:1)}function f(a){v?e(a):w.push(a)}null==document.readyState&&document.addEventListener&&(document.addEventListener("DOMContentLoaded",function y(){document.removeEventListener("DOMContentLoaded",y,!1),document.readyState="complete"},!1),document.readyState="loading");var g=a.document,h=g.documentElement,i="load",j=!1,k="on"+i,l="complete",m="readyState",n="attachEvent",o="detachEvent",p="addEventListener",q="DOMContentLoaded",r="onreadystatechange",s="removeEventListener",t=p in g,u=j,v=j,w=[];if(g[m]===l)e(b);else if(t)g[p](q,c,j),a[p](i,c,j);else{g[n](r,c),a[n](k,c);try{u=null==a.frameElement&&h}catch(x){}u&&u.doScroll&&!function z(){if(!v){try{u.doScroll("left")}catch(a){return e(z,50)}d(),b()}}()}return f.version="1.4.0",f.isReady=function(){return v},f}a.exports="undefined"!=typeof window&&b(window)},function(a,b,c){var d=c(5),e=function(a){function b(a,b){for(var c in b)a[c]=b[c];return a}var c=1,e=d.defclass({constructor:function(a){c++,this.parent=null,this.children={},this.id=c,this.name="n"+c,null!=a&&(this.name=a),this.x=0,this.y=0,this.z=0,this.width=0,this.height=0},resize:function(a,b){null!=a&&(this.width=a),null!=b&&(this.height=b)},moveTo:function(a,b,c){this.x=null!=a?a:this.x,this.y=null!=b?b:this.y,this.z=null!=c?c:this.z},add:function(a){var b=a.name;if(null!=this.children[b])throw"SceneGraph: child with that name already exists: "+b;this.children[b]=a,a.parent=this}}),f=d(e,function(b){this.constructor=function(){b.constructor.call(this,"root"),this.properties=a}}),g=d(e,function(a){function c(c,d){if(a.constructor.call(this,c),this.properties={fill:"#000"},null!=d)b(this.properties,d);else if(null!=c&&"string"!=typeof c)throw"SceneGraph: invalid node name"}this.Group=d.extend(this,{constructor:c,type:"group"}),this.Rect=d.extend(this,{constructor:c,type:"rect"}),this.Text=d.extend(this,{constructor:function(a){c.call(this),this.properties.text=a},type:"text"})}),h=new f;return this.Shape=g,this.root=h,this};a.exports=e},function(a,b){(function(a){b.extend=function(a,b){var c={};for(var d in a)a.hasOwnProperty(d)&&(c[d]=a[d]);if(null!=b)for(var e in b)b.hasOwnProperty(e)&&(c[e]=b[e]);return c},b.cssProps=function(a){var b=[];for(var c in a)a.hasOwnProperty(c)&&b.push(c+":"+a[c]);return b.join(";")},b.encodeHtmlEntity=function(a){for(var b=[],c=0,d=a.length-1;d>=0;d--)c=a.charCodeAt(d),b.unshift(c>128?["&#",c,";"].join(""):a[d]);return b.join("")},b.getNodeArray=function(b){var c=null;return"string"==typeof b?c=document.querySelectorAll(b):a.NodeList&&b instanceof a.NodeList?c=b:a.Node&&b instanceof a.Node?c=[b]:a.HTMLCollection&&b instanceof a.HTMLCollection?c=b:b instanceof Array?c=b:null===b&&(c=[]),c},b.imageExists=function(a,b){var c=new Image;c.onerror=function(){b.call(this,!1)},c.onload=function(){b.call(this,!0)},c.src=a},b.decodeHtmlEntity=function(a){return a.replace(/&#(\d+);/g,function(a,b){return String.fromCharCode(b)})},b.dimensionCheck=function(a){var b={height:a.clientHeight,width:a.clientWidth};return b.height&&b.width?b:!1},b.truthy=function(a){return"string"==typeof a?"true"===a||"yes"===a||"1"===a||"on"===a||"✓"===a:!!a}}).call(b,function(){return this}())},function(a,b,c){var d=encodeURIComponent,e=decodeURIComponent,f=c(6),g=c(7),h=/(\w+)\[(\d+)\]/,i=/\w+\.\w+/;b.parse=function(a){if("string"!=typeof a)return{};if(a=f(a),""===a)return{};"?"===a.charAt(0)&&(a=a.slice(1));for(var b={},c=a.split("&"),d=0;d",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.2",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b)}(this,document); -------------------------------------------------------------------------------- /coverage/js/respond.min.js: -------------------------------------------------------------------------------- 1 | /*! Respond.js v1.4.2: min/max-width media query polyfill * Copyright 2013 Scott Jehl 2 | * Licensed under https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT 3 | * */ 4 | 5 | !function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='­',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;benableDebug(); 9 | $sent = $client->sendMentions($url); 10 | 11 | echo "Sent $sent mentions\n"; 12 | -------------------------------------------------------------------------------- /phpunit: -------------------------------------------------------------------------------- 1 | ./vendor/phpunit/phpunit/phpunit -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | tests 8 | 9 | 10 | 11 | 12 | src/IndieWeb 13 | 14 | src/IndieWeb/MentionClientTest.php 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/IndieWeb/MentionClient.php: -------------------------------------------------------------------------------- 1 | c . 49 | * @param string $target URL 50 | * @return mixed setting $this->c('pingbackServer', $target); 51 | */ 52 | public function discoverPingbackEndpoint($target) { 53 | 54 | if($this->c('supportsPingback', $target) === null) { 55 | $this->c('supportsPingback', $target, false); 56 | 57 | // First try a HEAD request and look for X-Pingback header 58 | if(!$this->c('headers', $target)) { 59 | $head = static::_head($target); 60 | $target = $head['url']; 61 | $this->c('headers', $target, $head['headers']); 62 | } 63 | 64 | $headers = $this->c('headers', $target); 65 | if(array_key_exists('X-Pingback', $headers)) { 66 | self::_debug("discoverPingbackEndpoint: Found pingback server in header"); 67 | $this->c('pingbackServer', $target, $headers['X-Pingback']); 68 | $this->c('supportsPingback', $target, true); 69 | } else { 70 | self::_debug("discoverPingbackEndpoint: No pingback server found in header, looking in the body now"); 71 | if(!$this->c('body', $target)) { 72 | $body = static::_get($target); 73 | $target = $body['url']; 74 | $this->c('body', $target, $body['body']); 75 | $this->_parseBody($target, $body['body']); 76 | } 77 | if($rels=$this->c('rels', $target)) { 78 | // If the mf2 parser is present, then rels will have been set, and use that instead 79 | if(count($rels)) { 80 | if(array_key_exists('pingback', $rels)) { 81 | $this->c('pingbackServer', $target, $rels['pingback'][0]); 82 | $this->c('supportsPingback', $target, true); 83 | } 84 | } 85 | } else { 86 | $body = $this->c('body', $target); 87 | if(preg_match("//i", $body, $match)) { 88 | $this->c('pingbackServer', $target, $match[1]); 89 | $this->c('supportsPingback', $target, true); 90 | } 91 | } 92 | } 93 | 94 | self::_debug("discoverPingbackEndpoint: pingback server: " . $this->c('pingbackServer', $target)); 95 | } 96 | 97 | return $this->c('pingbackServer', $target); 98 | } 99 | 100 | /** 101 | * Sends pingback to endpoints 102 | * @param $endpoint string URL for pingback listener 103 | * @param $source string originating post URL 104 | * @param $target string URL like permalink of target post 105 | * @return bool Successful response MUST contain a single string 106 | */ 107 | public static function sendPingbackToEndpoint($endpoint, $source, $target) { 108 | self::_debug("sendPingbackToEndpoint: Sending pingback now!"); 109 | 110 | $payload = static::xmlrpc_encode_request('pingback.ping', array($source, $target)); 111 | 112 | $response = static::_post($endpoint, $payload, array( 113 | 'Content-type: application/xml' 114 | )); 115 | 116 | if($response['code'] != 200 || empty($response['body'])) 117 | return false; 118 | 119 | // collapse whitespace just to be safe 120 | $body = strtolower(preg_replace('/\s+/', '', $response['body'])); 121 | 122 | // successful response MUST contain a single string 123 | return $body && strpos($body, '') === false && strpos($body, '') !== false; 124 | } 125 | 126 | /** 127 | * Public function to send pingbacks to $targetURL 128 | * @param $sourceURL string URL for source of pingback 129 | * @param $targetURL string URL for destination of pingback 130 | * @return bool runs sendPingbackToEndpoint(). 131 | * @see MentionClient::sendPingbackToEndpoint() 132 | */ 133 | public function sendPingback($sourceURL, $targetURL) { 134 | 135 | // If we haven't discovered the pingback endpoint yet, do it now 136 | if($this->c('supportsPingback', $targetURL) === null) { 137 | $this->discoverPingbackEndpoint($targetURL); 138 | } 139 | 140 | $pingbackServer = $this->c('pingbackServer', $targetURL); 141 | if($pingbackServer) { 142 | self::_debug("sendPingback: Sending to pingback server: " . $pingbackServer); 143 | return self::sendPingbackToEndpoint($pingbackServer, $sourceURL, $targetURL); 144 | } else { 145 | return false; 146 | } 147 | } 148 | 149 | /** 150 | * Parses body of html. Protected method. 151 | * @param $target string the URL of the target page 152 | * @param $html string the HTML of page 153 | */ 154 | protected function _parseBody($target, $html) { 155 | if(class_exists('\Mf2\Parser') && $this->usemf2) { 156 | $parser = new \Mf2\Parser($html, $target); 157 | list($rels, $alternates) = $parser->parseRelsAndAlternates(); 158 | $this->c('rels', $target, $rels); 159 | } 160 | } 161 | 162 | /** 163 | * finds webmention endpoints in the body. protected function 164 | * @param $body 165 | * @param string $targetURL 166 | * @return bool 167 | */ 168 | protected function _findWebmentionEndpointInHTML($body, $targetURL = false) { 169 | $endpoint = false; 170 | 171 | $body = preg_replace('//Us', '', $body); 172 | if(preg_match('/<(?:link|a)[ ]+href="([^"]*)"[ ]+rel="[^" ]* ?webmention ?[^" ]*"[ ]*\/?>/i', $body, $match) 173 | || preg_match('/<(?:link|a)[ ]+rel="[^" ]* ?webmention ?[^" ]*"[ ]+href="([^"]*)"[ ]*\/?>/i', $body, $match)) { 174 | $endpoint = $match[1]; 175 | } 176 | if($endpoint !== false && $targetURL && function_exists('\Mf2\resolveUrl')) { 177 | // Resolve the URL if it's relative 178 | $endpoint = \Mf2\resolveUrl($targetURL, $endpoint); 179 | } 180 | return $endpoint; 181 | } 182 | 183 | /** 184 | * @param $link_header 185 | * @param string $targetURL 186 | * @return bool 187 | */ 188 | protected function _findWebmentionEndpointInHeader($link_header, $targetURL = false) { 189 | $endpoint = false; 190 | if(preg_match('~<((?:https?://)?[^>]+)>; rel="?(?:https?://webmention.org/?|webmention)"?~', $link_header, $match)) { 191 | $endpoint = $match[1]; 192 | } 193 | if($endpoint && $targetURL && function_exists('\Mf2\resolveUrl')) { 194 | // Resolve the URL if it's relative 195 | $endpoint = \Mf2\resolveUrl($targetURL, $endpoint); 196 | } 197 | return $endpoint; 198 | } 199 | 200 | /** 201 | * Finds webmention endpoints at URL. Examines header request. 202 | * Also modifies $this->c to indicate if $target accepts webmention 203 | * @param $target string the URL to examine for endpoints. 204 | * @return mixed 205 | */ 206 | public function discoverWebmentionEndpoint($target) { 207 | 208 | if($this->c('supportsWebmention', $target) === null) { 209 | $this->c('supportsWebmention', $target, false); 210 | 211 | // First try a HEAD request and look for Link header 212 | if(!$this->c('headers', $target)) { 213 | $head = static::_head($target); 214 | $target = $head['url']; 215 | $this->c('headers', $target, $head['headers']); 216 | } 217 | 218 | $headers = $this->c('headers', $target); 219 | 220 | $link_header = false; 221 | 222 | if(array_key_exists('Link', $headers)) { 223 | if(is_array($headers['Link'])) { 224 | $link_header = implode(", ", $headers['Link']); 225 | } else { 226 | $link_header = $headers['Link']; 227 | } 228 | } 229 | 230 | if($link_header && ($endpoint=$this->_findWebmentionEndpointInHeader($link_header, $target))) { 231 | self::_debug("discoverWebmentionEndpoint: Found webmention server in header"); 232 | $this->c('webmentionServer', $target, $endpoint); 233 | $this->c('supportsWebmention', $target, true); 234 | } else { 235 | self::_debug("discoverWebmentionEndpoint: No webmention server found in header, looking in body now"); 236 | if(!$this->c('body', $target)) { 237 | $body = static::_get($target); 238 | $target = $body['url']; 239 | $this->c('body', $target, $body['body']); 240 | $this->_parseBody($target, $body['body']); 241 | } 242 | if($rels=$this->c('rels', $target)) { 243 | // If the mf2 parser is present, then rels will have been set, so use that instead 244 | if(count($rels)) { 245 | if(array_key_exists('webmention', $rels)) { 246 | $endpoint = $rels['webmention'][0]; 247 | $this->c('webmentionServer', $target, $endpoint); 248 | $this->c('supportsWebmention', $target, true); 249 | } elseif(array_key_exists('http://webmention.org/', $rels) || array_key_exists('http://webmention.org', $rels)) { 250 | $endpoint = $rels[array_key_exists('http://webmention.org/', $rels) ? 'http://webmention.org/' : 'http://webmention.org'][0]; 251 | $this->c('webmentionServer', $target, $endpoint); 252 | $this->c('supportsWebmention', $target, true); 253 | } 254 | } 255 | } else { 256 | if($endpoint=$this->_findWebmentionEndpointInHTML($this->c('body', $target), $target)) { 257 | $this->c('webmentionServer', $target, $endpoint); 258 | $this->c('supportsWebmention', $target, true); 259 | } 260 | } 261 | } 262 | 263 | self::_debug("discoverWebmentionEndpoint: webmention server: " . $this->c('webmentionServer', $target)); 264 | } 265 | 266 | return $this->c('webmentionServer', $target); 267 | } 268 | 269 | /** 270 | * Static function can send a webmention to an endpoint via static::_post 271 | * @param $endpoint string URL of endpoint detected 272 | * @param $source string URL of originating post (other server will check probably) 273 | * @param $target string URL of target post 274 | * @param array $additional extra optional stuff that will be included in payload. 275 | * @return array 276 | */ 277 | public static function sendWebmentionToEndpoint($endpoint, $source, $target, $additional = array()) { 278 | 279 | self::_debug("sendWebmentionToEndpoint: Sending webmention now!"); 280 | 281 | $payload = http_build_query(array_merge(array( 282 | 'source' => $source, 283 | 'target' => $target 284 | ), $additional)); 285 | 286 | return static::_post($endpoint, $payload, array( 287 | 'Content-type: application/x-www-form-urlencoded', 288 | 'Accept: application/json, */*;q=0.8' 289 | )); 290 | } 291 | 292 | /** 293 | * Sends webmention to a target url. may use 294 | * @param $sourceURL 295 | * @param $targetURL 296 | * @param array $additional 297 | * @return array|bool 298 | * @see MentionClient::sendWebmentionToEndpoint() 299 | */ 300 | public function sendWebmention($sourceURL, $targetURL, $additional = array()) { 301 | 302 | // If we haven't discovered the webmention endpoint yet, do it now 303 | if($this->c('supportsWebmention', $targetURL) === null) { 304 | $this->discoverWebmentionEndpoint($targetURL); 305 | } 306 | 307 | $webmentionServer = $this->c('webmentionServer', $targetURL); 308 | if($webmentionServer) { 309 | self::_debug("sendWebmention: Sending to webmention server: " . $webmentionServer); 310 | return self::sendWebmentionToEndpoint($webmentionServer, $sourceURL, $targetURL, $additional); 311 | } else { 312 | return false; 313 | } 314 | } 315 | 316 | /** 317 | * Scans outgoing links in block of text $input. 318 | * @param $input string html block. 319 | * @return array array of unique links or empty. 320 | */ 321 | public static function findOutgoingLinks($input) { 322 | // Find all outgoing links in the source 323 | if(is_string($input)) { 324 | preg_match_all("/]+href=.(https?:\/\/[^'\"]+)/i", $input, $matches); 325 | return array_unique($matches[1]); 326 | } elseif (is_array($input) && array_key_exists('items', $input) && array_key_exists(0, $input['items'])) { 327 | $links = array(); 328 | 329 | // Find links in the content HTML 330 | $item = $input['items'][0]; 331 | 332 | if (array_key_exists('content', $item['properties'])) { 333 | if (is_array($item['properties']['content'][0])) { 334 | $html = $item['properties']['content'][0]['html']; 335 | $links = array_merge($links, self::findOutgoingLinks($html)); 336 | } else { 337 | $text = $item['properties']['content'][0]; 338 | $links = array_merge($links, self::findLinksInText($text)); 339 | } 340 | } 341 | 342 | // Look at all properties of the item and collect all the ones that look like URLs 343 | $links = array_merge($links, self::findLinksInJSON($item)); 344 | 345 | return array_unique($links); 346 | } else { 347 | return array(); 348 | } 349 | } 350 | 351 | /** 352 | * find all links in text. 353 | * @param $input string text block 354 | * @return mixed array of links in text block. 355 | */ 356 | public static function findLinksInText($input) { 357 | preg_match_all('/https?:\/\/[^ ]+/', $input, $matches); 358 | return array_unique($matches[0]); 359 | } 360 | 361 | /** 362 | * find links in JSON input string. 363 | * @param $input string JSON object. 364 | * @return array of links in JSON object. 365 | */ 366 | public static function findLinksInJSON($input) { 367 | $links = array(); 368 | // This recursively iterates over the whole input array and searches for 369 | // everything that looks like a URL regardless of its depth or property name 370 | foreach(new \RecursiveIteratorIterator(new \RecursiveArrayIterator($input)) as $key => $value) { 371 | if(substr($value, 0, 7) == 'http://' || substr($value, 0, 8) == 'https://') 372 | $links[] = $value; 373 | } 374 | return $links; 375 | } 376 | 377 | /** 378 | * Tries to send webmention and pingbacks to each link on $sourceURL. Depends on Microformats2 379 | * @param $sourceURL string URL to examine to send mentions to 380 | * @param bool $sourceBody if true will search for outgoing links with this (string). 381 | * @return int 382 | * @see \Mf2\parse 383 | */ 384 | public function sendMentions($sourceURL, $sourceBody = false) { 385 | if($sourceBody) { 386 | $this->_sourceBody = $sourceBody; 387 | $this->_links = self::findOutgoingLinks($sourceBody); 388 | } else { 389 | $body = static::_get($sourceURL); 390 | $this->_sourceBody = $body['body']; 391 | $parsed = \Mf2\parse($this->_sourceBody, $sourceURL); 392 | $this->_links = self::findOutgoingLinks($parsed); 393 | } 394 | 395 | $totalAccepted = 0; 396 | 397 | foreach($this->_links as $target) { 398 | self::_debug("sendMentions: Checking $target for webmention and pingback endpoints"); 399 | 400 | if($this->sendFirstSupportedMention($sourceURL, $target)) { 401 | $totalAccepted++; 402 | } 403 | } 404 | 405 | return $totalAccepted; 406 | } 407 | 408 | /** 409 | * @param $source 410 | * @param $target 411 | * @return bool|string 412 | */ 413 | public function sendFirstSupportedMention($source, $target) { 414 | 415 | $accepted = false; 416 | 417 | // Look for a webmention endpoint first 418 | if($this->discoverWebmentionEndpoint($target)) { 419 | $result = $this->sendWebmention($source, $target); 420 | if($result && 421 | ($result['code'] == 200 422 | || $result['code'] == 201 423 | || $result['code'] == 202)) { 424 | $accepted = 'webmention'; 425 | } 426 | // Only look for a pingback server if we didn't find a webmention server 427 | } else if($this->discoverPingbackEndpoint($target)) { 428 | $result = $this->sendPingback($source, $target); 429 | if($result) { 430 | $accepted = 'pingback'; 431 | } 432 | } 433 | 434 | return $accepted; 435 | } 436 | 437 | /** 438 | * Enables debug messages to appear during activity. Not recommended for production use. 439 | * @codeCoverageIgnore 440 | */ 441 | public static function enableDebug() { 442 | self::$_debugEnabled = true; 443 | } 444 | /** 445 | * @codeCoverageIgnore 446 | */ 447 | private static function _debug($msg) { 448 | if(self::$_debugEnabled) 449 | echo "\t" . $msg . "\n"; 450 | } 451 | 452 | /** 453 | * @param $url 454 | * @return array 455 | * @codeCoverageIgnore 456 | */ 457 | protected static function _head($url, $headers=array()) { 458 | if(self::$_userAgent) 459 | $headers[] = 'User-Agent: '.self::$_userAgent; 460 | 461 | $ch = curl_init($url); 462 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 463 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 464 | curl_setopt($ch, CURLOPT_HEADER, true); 465 | curl_setopt($ch, CURLOPT_NOBODY, true); 466 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 467 | if (self::$_proxy) curl_setopt($ch, CURLOPT_PROXY, self::$_proxy); 468 | $response = curl_exec($ch); 469 | return array( 470 | 'code' => curl_getinfo($ch, CURLINFO_HTTP_CODE), 471 | 'headers' => self::_parse_headers(trim($response)), 472 | 'url' => curl_getinfo($ch, CURLINFO_EFFECTIVE_URL) 473 | ); 474 | } 475 | 476 | /** 477 | * Protected static function 478 | * @param $url string URL to grab through curl. 479 | * @return array with keys 'code' 'headers' and 'body' 480 | * @codeCoverageIgnore 481 | */ 482 | protected static function _get($url, $headers=array()) { 483 | if(self::$_userAgent) 484 | $headers[] = 'User-Agent: '.self::$_userAgent; 485 | 486 | $ch = curl_init($url); 487 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 488 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 489 | curl_setopt($ch, CURLOPT_HEADER, true); 490 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 491 | if (self::$_proxy) curl_setopt($ch, CURLOPT_PROXY, self::$_proxy); 492 | $response = curl_exec($ch); 493 | $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); 494 | return array( 495 | 'code' => curl_getinfo($ch, CURLINFO_HTTP_CODE), 496 | 'headers' => self::_parse_headers(trim(substr($response, 0, $header_size))), 497 | 'body' => substr($response, $header_size), 498 | 'url' => curl_getinfo($ch, CURLINFO_EFFECTIVE_URL) 499 | ); 500 | } 501 | 502 | /** 503 | * @param $url 504 | * @param $body 505 | * @param array $headers 506 | * @return array 507 | * @codeCoverageIgnore 508 | */ 509 | protected static function _post($url, $body, $headers=array()) { 510 | if(self::$_userAgent) 511 | $headers[] = 'User-Agent: '.self::$_userAgent; 512 | 513 | $ch = curl_init($url); 514 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 515 | curl_setopt($ch, CURLOPT_POST, true); 516 | curl_setopt($ch, CURLOPT_POSTFIELDS, $body); 517 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 518 | curl_setopt($ch, CURLOPT_HEADER, true); 519 | if (self::$_proxy) curl_setopt($ch, CURLOPT_PROXY, self::$_proxy); 520 | $response = curl_exec($ch); 521 | self::_debug($response); 522 | $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); 523 | return array( 524 | 'code' => curl_getinfo($ch, CURLINFO_HTTP_CODE), 525 | 'headers' => self::_parse_headers(trim(substr($response, 0, $header_size))), 526 | 'body' => substr($response, $header_size) 527 | ); 528 | } 529 | 530 | /** 531 | * Protected static function to parse headers. 532 | * @param $headers 533 | * @return array 534 | */ 535 | protected static function _parse_headers($headers) { 536 | $retVal = array(); 537 | $fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $headers)); 538 | foreach($fields as $field) { 539 | if (preg_match('/([^:]+): (.+)/m', $field, $match)) { 540 | $match[1] = preg_replace_callback('/(?<=^|[\x09\x20\x2D])./', function($m) { 541 | return strtoupper($m[0]); 542 | }, strtolower(trim($match[1]))); 543 | // If there's already a value set for the header name being returned, turn it into an array and add the new value 544 | $match[1] = preg_replace_callback('/(?<=^|[\x09\x20\x2D])./', function($m) { 545 | return strtoupper($m[0]); 546 | }, strtolower(trim($match[1]))); 547 | if (isset($retVal[$match[1]])) { 548 | if (!is_array($retVal[$match[1]])) 549 | $retVal[$match[1]] = array($retVal[$match[1]]); 550 | $retVal[$match[1]][] = $match[2]; 551 | } else { 552 | $retVal[$match[1]] = trim($match[2]); 553 | } 554 | } 555 | } 556 | return $retVal; 557 | } 558 | 559 | /** 560 | * Static function for XML-RPC encoding request. 561 | * @param $method string goes into MethodName XML tag 562 | * @param $params array set of strings that go into param/value XML tags. 563 | * @return string 564 | */ 565 | public static function xmlrpc_encode_request($method, $params) { 566 | $xml = ''; 567 | $xml .= ''; 568 | $xml .= ''.htmlspecialchars($method).''; 569 | $xml .= ''; 570 | foreach ($params as $param) { 571 | $xml .= ''.htmlspecialchars($param).''; 572 | } 573 | $xml .= ''; 574 | 575 | return $xml; 576 | } 577 | 578 | /** 579 | * Caching key/value system for MentionClient 580 | * @param $type 581 | * @param $url 582 | * @param mixed $val If not null, is set to default value 583 | * @return mixed 584 | */ 585 | public function c($type, $url, $val=null) { 586 | // Create the empty record if it doesn't yet exist 587 | $key = '_'.$type; 588 | 589 | if(!array_key_exists($url, $this->{$key})) { 590 | $this->{$key}[$url] = null; 591 | } 592 | 593 | if($val !== null) { 594 | $this->{$key}[$url] = $val; 595 | } 596 | 597 | return $this->{$key}[$url]; 598 | } 599 | } 600 | -------------------------------------------------------------------------------- /src/IndieWeb/MentionClientTest.php: -------------------------------------------------------------------------------- 1 | setAccessible(true); 15 | return $method->invokeArgs($this, $args); 16 | } 17 | 18 | public static function __callStatic($method, $args) { 19 | $method = new \ReflectionMethod('IndieWeb\MentionClient', $method); 20 | $method->setAccessible(true); 21 | return $method->invokeArgs(null, $args); 22 | } 23 | 24 | protected static function _head($url, $headers=array()) { 25 | self::$_redirects_remaining = 5; 26 | $response = self::_read_file($url); 27 | return array( 28 | 'code' => $response['code'], 29 | 'headers' => $response['headers'], 30 | 'url' => $response['url'] 31 | ); 32 | } 33 | 34 | protected static function _get($url, $headers=array()) { 35 | self::$_redirects_remaining = 5; 36 | return self::_read_file($url); 37 | } 38 | 39 | protected static function _post($url, $body, $headers=array()) { 40 | return self::_read_file($url); 41 | } 42 | 43 | private static function _read_file($url) { 44 | if(self::$dataDir) { 45 | $dataDir = self::$dataDir; 46 | } else { 47 | $dataDir = dirname(__FILE__).'/../../tests/data/'; 48 | } 49 | 50 | $filename = $dataDir.preg_replace('/https?:\/\//', '', $url); 51 | if(!file_exists($filename)) { 52 | $filename = dirname(__FILE__).'/../../tests/data/404.response.txt'; 53 | } 54 | $response = file_get_contents($filename); 55 | 56 | $split = explode("\r\n\r\n", $response); 57 | if(count($split) != 2) { 58 | throw new \Exception("Invalid file contents, check that newlines are CRLF: $url"); 59 | } 60 | list($headers, $body) = $split; 61 | 62 | if(preg_match('/HTTP\/1\.1 (\d+)/', $headers, $match)) { 63 | $code = $match[1]; 64 | } 65 | 66 | $headers = preg_replace('/HTTP\/1\.1 \d+ .+/', '', $headers); 67 | $parsedHeaders = self::_parse_headers($headers); 68 | 69 | if(($code == 302 || $code == 301) && array_key_exists('Location', $parsedHeaders)) { 70 | $effectiveUrl = \mf2\resolveUrl($url, $parsedHeaders['Location']); 71 | if(self::$_redirects_remaining > 0) { 72 | self::$_redirects_remaining--; 73 | return self::_read_file($effectiveUrl); 74 | } else { 75 | return [ 76 | 'code' => 0, 77 | 'headers' => $parsedHeaders, 78 | 'body' => $body, 79 | 'error' => 'too_many_redirects', 80 | 'error_description' => '', 81 | 'url' => $effectiveUrl 82 | ]; 83 | } 84 | } else { 85 | $effectiveUrl = $url; 86 | } 87 | 88 | return array( 89 | 'code' => $code, 90 | 'headers' => $parsedHeaders, 91 | 'body' => $body, 92 | 'url' => $effectiveUrl 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/DiscoverTest.php: -------------------------------------------------------------------------------- 1 | client = new IndieWeb\MentionClientTest(false, 'empty'); 8 | } 9 | 10 | public function testDiscoverWebmentionEndpoint() { 11 | $headers = "HTTP/1.1 200 OK\r 12 | Link: ; rel=\"webmention\"\r 13 | Link: ; rel=\"me\"\r 14 | "; 15 | 16 | $target = 'http://example.com/'; 17 | $this->client->c('headers', $target, IndieWeb\MentionClientTest::_parse_headers($headers)); 18 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 19 | $this->assertEquals('http://aaronparecki.com/webmention.php', $endpoint); 20 | } 21 | 22 | public function testDiscoverPingbackEndpoint() { 23 | $headers = "HTTP/1.1 200 OK\r 24 | X-Pingback: http://pingback.me/webmention?forward=http%3A%2F%2Faaronparecki.com%2Fwebmention.php\r 25 | Link: ; rel=\"me\"\r 26 | "; 27 | 28 | $target = 'http://example.com/'; 29 | $this->client->c('headers', $target, IndieWeb\MentionClientTest::_parse_headers($headers)); 30 | $endpoint = $this->client->discoverPingbackEndpoint($target); 31 | $this->assertEquals('http://pingback.me/webmention?forward=http%3A%2F%2Faaronparecki.com%2Fwebmention.php', $endpoint); 32 | } 33 | 34 | public function testDiscoverWebmentionEndpointInHeader() { 35 | $target = 'http://target.example.com/header.html'; 36 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 37 | $this->assertEquals('http://webmention-endpoint.example/queued-response', $endpoint); 38 | } 39 | 40 | public function testDiscoverPingbackEndpointInHeader() { 41 | $target = 'http://target.example.com/header.html'; 42 | $endpoint = $this->client->discoverPingbackEndpoint($target); 43 | $this->assertEquals('http://pingback-endpoint.example/valid-response', $endpoint); 44 | } 45 | 46 | public function testDiscoverWebmentionEndpointInBodyLink() { 47 | $target = 'http://target.example.com/body-link.html'; 48 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 49 | $this->assertEquals('http://webmention.example/webmention', $endpoint); 50 | 51 | $target = 'http://target.example.com/body-link-org.html'; 52 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 53 | $this->assertEquals('http://webmention.example/webmention', $endpoint); 54 | 55 | $target = 'http://target.example.com/body-link-org2.html'; 56 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 57 | $this->assertEquals('http://webmention.example/webmention', $endpoint); 58 | } 59 | 60 | public function testDiscoverPingbackEndpointInBodyLink() { 61 | $target = 'http://target.example.com/body-link.html'; 62 | $endpoint = $this->client->discoverPingbackEndpoint($target); 63 | $this->assertEquals('http://webmention.example/pingback', $endpoint); 64 | } 65 | 66 | public function testDiscoverPingbackEndpointInBodyWithoutMf2() { 67 | $target = 'http://target.example.com/body-link.html'; 68 | $this->client->usemf2 = false; 69 | $endpoint = $this->client->discoverPingbackEndpoint($target); 70 | $this->assertEquals('http://webmention.example/pingback', $endpoint); 71 | } 72 | 73 | public function testDiscoverWebmentionEndpointInBodyA() { 74 | $target = 'http://target.example.com/body-a.html'; 75 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 76 | $this->assertEquals('http://webmention.example/webmention', $endpoint); 77 | } 78 | 79 | public function testDiscoverWebmentionEndpointInBodyAWithoutMf2() { 80 | $target = 'http://target.example.com/body-a.html'; 81 | $this->client->usemf2 = false; 82 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 83 | $this->assertEquals('http://webmention.example/webmention', $endpoint); 84 | } 85 | 86 | public function testShouldNotDiscoverWebmentionEndpointInBodyComment() { 87 | $target = 'http://target.example.com/false-endpoint-in-comment.html'; 88 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 89 | $this->assertEquals('http://webmention.example/correct', $endpoint); 90 | } 91 | 92 | public function testDiscoverWebmentionEndpointInDocumentOrder1() { 93 | $target = 'http://target.example.com/document-order-1.html'; 94 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 95 | $this->assertEquals('http://webmention.example/link', $endpoint); 96 | } 97 | 98 | public function testDiscoverWebmentionEndpointInDocumentOrder2() { 99 | $target = 'http://target.example.com/document-order-2.html'; 100 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 101 | $this->assertEquals('http://webmention.example/a', $endpoint); 102 | } 103 | 104 | public function testDiscoverWebmentionEndpointIsEmptyString() { 105 | $target = 'http://target.example.com/empty-string.html'; 106 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 107 | $this->assertEquals('http://target.example.com/empty-string.html', $endpoint); 108 | } 109 | 110 | public function testDiscoverWebmentionEndpointIsPathRelative() { 111 | $target = 'http://target.example.com/relative/path-relative-endpoint.html'; 112 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 113 | $this->assertEquals('http://target.example.com/relative/relative', $endpoint); 114 | } 115 | 116 | public function testDiscoverEndpointAfterRedirected() { 117 | $target = 'http://target.example.com/redirect.html'; 118 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 119 | $this->assertEquals('http://target.example.com/relative/webmention', $endpoint); 120 | } 121 | 122 | public function testDiscoverWebmentionEndpointInWebmentionRocksTest1() { 123 | $target = "http://target.example.com/webmention-rocks-test-1.html"; 124 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 125 | $this->assertEquals("http://target.example.com/test/1/webmention", $endpoint); 126 | } 127 | 128 | public function testDiscoverWebmentionEndpointInWebmentionRocksTest2() { 129 | $target = "http://target.example.com/webmention-rocks-test-2.html"; 130 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 131 | $this->assertEquals("https://webmention.rocks/test/2/webmention", $endpoint); 132 | } 133 | 134 | public function testDiscoverWebmentionEndpointInWebmentionRocksTest3() { 135 | $target = "http://target.example.com/webmention-rocks-test-3.html"; 136 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 137 | $this->assertEquals("http://target.example.com/test/3/webmention", $endpoint); 138 | } 139 | 140 | public function testDiscoverWebmentionEndpointInWebmentionRocksTest4() { 141 | $target = "http://target.example.com/webmention-rocks-test-4.html"; 142 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 143 | $this->assertEquals("https://webmention.rocks/test/4/webmention", $endpoint); 144 | } 145 | 146 | public function testDiscoverWebmentionEndpointInWebmentionRocksTest5() { 147 | $target = "http://target.example.com/webmention-rocks-test-5.html"; 148 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 149 | $this->assertEquals("http://target.example.com/test/5/webmention", $endpoint); 150 | } 151 | 152 | public function testDiscoverWebmentionEndpointInWebmentionRocksTest6() { 153 | $target = "http://target.example.com/webmention-rocks-test-6.html"; 154 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 155 | $this->assertEquals("https://webmention.rocks/test/6/webmention", $endpoint); 156 | } 157 | 158 | public function testDiscoverWebmentionEndpointInWebmentionRocksTest7() { 159 | $target = "http://target.example.com/webmention-rocks-test-7.html"; 160 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 161 | $this->assertEquals("https://webmention.rocks/test/7/webmention", $endpoint); 162 | } 163 | 164 | public function testDiscoverWebmentionEndpointInWebmentionRocksTest8() { 165 | $target = "http://target.example.com/webmention-rocks-test-8.html"; 166 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 167 | $this->assertEquals("https://webmention.rocks/test/8/webmention", $endpoint); 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /tests/FindOutgoingLinksTest.php: -------------------------------------------------------------------------------- 1 | example

'; 6 | $links = IndieWeb\MentionClientTest::findOutgoingLinks($html); 7 | $this->assertEquals(array('http://example.com/'), $links); 8 | } 9 | 10 | public function testFindLinkInHEntryContent() { 11 | $html = '
'; 12 | $mf2 = Mf2\parse($html, 'http://source.example.net/'); 13 | $links = IndieWeb\MentionClientTest::findOutgoingLinks($mf2); 14 | $this->assertEquals(array('http://example.com/'), $links); 15 | 16 | $html = '
'; 17 | $mf2 = Mf2\parse($html, 'http://source.example.net/'); 18 | $links = IndieWeb\MentionClientTest::findOutgoingLinks($mf2); 19 | $this->assertEquals(array('http://example.com/'), $links); 20 | } 21 | 22 | public function testFindLinkInHEntryProperty() { 23 | $html = '
#example
'; 24 | $mf2 = Mf2\parse($html, 'http://source.example.net/'); 25 | $links = IndieWeb\MentionClientTest::findOutgoingLinks($mf2); 26 | $this->assertEquals(array('http://example.com/'), $links); 27 | } 28 | 29 | public function testFindLinkInNestedHEntryProperty() { 30 | $html = '
'; 31 | $mf2 = Mf2\parse($html, 'http://source.example.net/'); 32 | $links = IndieWeb\MentionClientTest::findOutgoingLinks($mf2); 33 | $this->assertEquals(array('http://example.com/'), $links); 34 | } 35 | 36 | public function testFindLinksInProperties() { 37 | $input = json_decode('{ 38 | "type": [ 39 | "h-entry" 40 | ], 41 | "properties": { 42 | "author": [ 43 | { 44 | "type": [ 45 | "h-card" 46 | ], 47 | "properties": { 48 | "url": [ 49 | "http://example.com/" 50 | ], 51 | "name": [ 52 | "example" 53 | ] 54 | }, 55 | "value": "example" 56 | } 57 | ], 58 | "name": [ 59 | "#example" 60 | ], 61 | "url": [ 62 | "http://source.example.net/" 63 | ] 64 | } 65 | }'); 66 | $links = IndieWeb\MentionClientTest::findLinksInJSON($input); 67 | $this->assertEquals(array('http://example.com/','http://source.example.net/'), $links); 68 | } 69 | 70 | public function testFindNoLinksInHEntry() { 71 | $html = '
example
'; 72 | $mf2 = Mf2\parse($html, 'http://source.example.net/'); 73 | $links = IndieWeb\MentionClientTest::findOutgoingLinks($mf2); 74 | $this->assertEquals(array(), $links); 75 | } 76 | 77 | public function testFindNoLinksInHTML() { 78 | $html = '

Hello World

'; 79 | $mf2 = Mf2\parse($html, 'http://source.example.net/'); 80 | $links = IndieWeb\MentionClientTest::findOutgoingLinks($mf2); 81 | $this->assertEquals(array(), $links); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /tests/LinkHeaderParserTest.php: -------------------------------------------------------------------------------- 1 | client = new IndieWeb\MentionClientTest(false, 'empty'); 8 | } 9 | 10 | public function testParsesMultipleHeadersToArray() { 11 | $headers = "HTTP/1.1 200 OK\r 12 | Link: ; rel=\"http://webmention.org/\"\r 13 | Link: ; rel=\"webmention\"\r 14 | Link: ; rel=\"me\"\r 15 | "; 16 | 17 | $headers = IndieWeb\MentionClientTest::_parse_headers($headers); 18 | $this->assertInternalType('array', $headers['Link']); 19 | } 20 | 21 | public function testNormalizesHeaderNameCase() { 22 | $headers = "HTTP/1.1 200 OK\r 23 | One-two: three\r 24 | four-five: six\r 25 | Seven-Eight: nine\r 26 | TEN-ELEVEN: twelve\r 27 | "; 28 | 29 | $headers = IndieWeb\MentionClientTest::_parse_headers($headers); 30 | $this->assertArrayHasKey('One-Two', $headers); 31 | $this->assertArrayHasKey('Four-Five', $headers); 32 | $this->assertArrayHasKey('Seven-Eight', $headers); 33 | $this->assertArrayHasKey('Ten-Eleven', $headers); 34 | } 35 | 36 | public function testFindWebmentionLinkHeader() { 37 | $headers = "HTTP/1.1 200 OK\r 38 | Server: nginx/1.0.14\r 39 | Date: Thu, 04 Jul 2013 15:56:21 GMT\r 40 | Content-Type: text/html; charset=UTF-8\r 41 | Connection: keep-alive\r 42 | X-Powered-By: PHP/5.3.13\r 43 | X-Pingback: http://pingback.me/webmention?forward=http%3A%2F%2Faaronparecki.com%2Fwebmention.php\r 44 | Link: ; rel=\"http://webmention.org/\"\r 45 | Link: ; rel=\"webmention\"\r 46 | Link: ; rel=\"me\"\r 47 | "; 48 | 49 | $target = 'http://example.com/'; 50 | $this->client->c('headers', $target, IndieWeb\MentionClientTest::_parse_headers($headers)); 51 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 52 | $this->assertEquals('http://aaronparecki.com/webmention.php', $endpoint); 53 | } 54 | 55 | public function testFindWebmentionHeaderRelWebmention() { 56 | $header = '; rel="webmention"'; 57 | $endpoint = $this->client->_findWebmentionEndpointInHeader($header); 58 | $this->assertEquals('http://example.com/webmention', $endpoint); 59 | } 60 | 61 | public function testFindWebmentionHeaderRelWebmentionOrg() { 62 | $header = '; rel="http://webmention.org"'; 63 | $endpoint = $this->client->_findWebmentionEndpointInHeader($header); 64 | $this->assertEquals('http://example.com/webmention', $endpoint); 65 | } 66 | 67 | public function testFindWebmentionHeaderRelWebmentionOrgSlash() { 68 | $header = '; rel="http://webmention.org/"'; 69 | $endpoint = $this->client->_findWebmentionEndpointInHeader($header); 70 | $this->assertEquals('http://example.com/webmention', $endpoint); 71 | } 72 | 73 | public function testFindWebmentionLinkHeaderWithMultipleLinks() { 74 | $headers = "HTTP/1.1 200 OK\r 75 | Link: ; rel=\"hub\", ; rel=\"hub\", ; rel=\"self\", ; rel=\"hub\", ; rel=\"self\", ; rel=\"http://webmention.org/\", ; rel=shortlink\r 76 | "; 77 | 78 | $target = 'http://aaronparecki.com/'; 79 | $this->client->c('headers', $target, IndieWeb\MentionClientTest::_parse_headers($headers)); 80 | $endpoint = $this->client->discoverWebmentionEndpoint($target); 81 | $this->assertEquals('http://notizblog.org/?webmention=endpoint', $endpoint); 82 | } 83 | 84 | public function testFindWebmentionHeaderRelativeUrl() { 85 | $header = '; rel="webmention"'; 86 | $endpoint = $this->client->_findWebmentionEndpointInHeader($header, 'http://example.com/post/1'); 87 | $this->assertEquals('http://example.com/webmention', $endpoint); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /tests/SendMentionsTest.php: -------------------------------------------------------------------------------- 1 | client = new IndieWeb\MentionClientTest(false, 'empty'); 8 | } 9 | 10 | public function testFromBodyWithNoLinks() { 11 | $total = $this->client->sendMentions('http://source.example.com/no-links.html', '

No links here

'); 12 | $this->assertEquals(0, $total); 13 | } 14 | 15 | public function testFromURLWithNoLinks() { 16 | $total = $this->client->sendMentions('http://source.example.com/no-links.html'); 17 | $this->assertEquals(0, $total); 18 | } 19 | 20 | public function testFromURLWithTwoValidLinks() { 21 | $total = $this->client->sendMentions('http://source.example.com/two-valid-links.html'); 22 | $this->assertEquals(2, $total); 23 | } 24 | 25 | public function testFromURLWithOneValidAndOneInvalidLink() { 26 | $total = $this->client->sendMentions('http://source.example.com/mixed-success-links.html'); 27 | $this->assertEquals(1, $total); 28 | } 29 | 30 | public function testDoesNotSendToLinksOutsideHEntry() { 31 | $total = $this->client->sendMentions('http://source.example.com/send-to-h-entry-links.html'); 32 | $this->assertEquals(1, $total); 33 | } 34 | 35 | public function testPrioritizesWebmentionEndpointOverPingback() { 36 | $result = $this->client->sendFirstSupportedMention('http://source.example.com/example.html', 'http://target.example.com/header.html'); 37 | $this->assertEquals('webmention', $result); 38 | } 39 | 40 | public function testFindsPingbackEndpointBecauseNoWebmentionEndpoint() { 41 | $result = $this->client->sendFirstSupportedMention('http://source.example.com/example.html', 'http://target.example.com/only-pingback.html'); 42 | $this->assertEquals('pingback', $result); 43 | } 44 | 45 | public function testDoesNotSendPingbackDespiteWebmentionFail() { 46 | $result = $this->client->sendFirstSupportedMention('http://source.example.com/example.html', 'http://target.example.com/webmention-failed.html'); 47 | $this->assertEquals(false, $result); 48 | } 49 | 50 | public function testSendsFailingPingback() { 51 | $result = $this->client->sendFirstSupportedMention('http://source.example.com/example.html', 'http://target.example.com/pingback-failed.html'); 52 | $this->assertEquals(false, $result); 53 | } 54 | 55 | public function testSendsFailingWebmention() { 56 | $result = $this->client->sendFirstSupportedMention('http://source.example.com/example.html', 'http://target.example.com/webmention-only-failed.html'); 57 | $this->assertEquals(false, $result); 58 | } 59 | 60 | public function testSendsWebmentionAndWasCreated() { 61 | $result = $this->client->sendFirstSupportedMention('http://source.example.com/example.html', 'http://target.example.com/webmention-created.html'); 62 | $this->assertEquals('webmention', $result); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /tests/SendPingbackTest.php: -------------------------------------------------------------------------------- 1 | client = new IndieWeb\MentionClientTest(false, 'empty'); 8 | } 9 | 10 | public function testNot200Response() { 11 | $endpoint = 'http://pingback-endpoint.example/404-response'; 12 | $response = $this->client->sendPingbackToEndpoint($endpoint, 'source', 'target'); 13 | $this->assertFalse($response); 14 | } 15 | 16 | public function testInvalidXMLResponse() { 17 | $endpoint = 'http://pingback-endpoint.example/invalid-xmlrpc'; 18 | $response = $this->client->sendPingbackToEndpoint($endpoint, 'source', 'target'); 19 | $this->assertFalse($response); 20 | } 21 | 22 | public function testInvalidBodyResponse() { 23 | $endpoint = 'http://pingback-endpoint.example/invalid-body'; 24 | $response = $this->client->sendPingbackToEndpoint($endpoint, 'source', 'target'); 25 | $this->assertFalse($response); 26 | } 27 | 28 | public function testInvalidRequest() { 29 | $endpoint = 'http://pingback-endpoint.example/invalid-request'; 30 | $response = $this->client->sendPingbackToEndpoint($endpoint, 'source', 'target'); 31 | $this->assertFalse($response); 32 | } 33 | 34 | public function testEmptyBodyResponse() { 35 | $endpoint = 'http://pingback-endpoint.example/empty-body'; 36 | $response = $this->client->sendPingbackToEndpoint($endpoint, 'source', 'target'); 37 | $this->assertTrue($response); 38 | } 39 | 40 | public function testValidResponse() { 41 | $endpoint = 'http://pingback-endpoint.example/valid-response'; 42 | $response = $this->client->sendPingbackToEndpoint($endpoint, 'source', 'target'); 43 | $this->assertTrue($response); 44 | } 45 | 46 | public function testSendPingbackNoEndpoint() { 47 | $target = 'http://pingback-target.example/no-endpoint.html'; 48 | $result = $this->client->sendPingback('http://source.example.com/', $target); 49 | $this->assertFalse($result); 50 | } 51 | 52 | public function testSendPingbackHasValidEndpoint() { 53 | $target = 'http://pingback-target.example/has-valid-endpoint.html'; 54 | $result = $this->client->sendPingback('http://source.example.com/', $target); 55 | $this->assertTrue($result); 56 | } 57 | 58 | public function testSendPingbackHasErroringEndpoint() { 59 | $target = 'http://pingback-target.example/has-erroring-endpoint.html'; 60 | $result = $this->client->sendPingback('http://source.example.com/', $target); 61 | $this->assertFalse($result); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/SendWebmentionTest.php: -------------------------------------------------------------------------------- 1 | client = new IndieWeb\MentionClientTest(false, 'empty'); 8 | } 9 | 10 | public function testNot200Response() { 11 | $endpoint = 'http://webmention-endpoint.example/404-response'; 12 | $response = $this->client->sendWebmentionToEndpoint($endpoint, 'source', 'target'); 13 | $this->assertInternalType('array', $response); 14 | $this->assertEquals(404, $response['code']); 15 | } 16 | 17 | public function testInvalidRequest() { 18 | $endpoint = 'http://webmention-endpoint.example/invalid-request'; 19 | $response = $this->client->sendWebmentionToEndpoint($endpoint, 'source', 'target'); 20 | $this->assertInternalType('array', $response); 21 | $this->assertEquals(400, $response['code']); 22 | } 23 | 24 | public function testValidResponse() { 25 | $endpoint = 'http://webmention-endpoint.example/queued-response'; 26 | $response = $this->client->sendWebmentionToEndpoint($endpoint, 'source', 'target'); 27 | $this->assertInternalType('array', $response); 28 | $this->assertEquals(202, $response['code']); 29 | } 30 | 31 | public function testSendWebmentionNoEndpoint() { 32 | $target = 'http://webmention-target.example/no-endpoint.html'; 33 | $response = $this->client->sendWebmention('http://source.example.com/', $target); 34 | $this->assertFalse($response); 35 | } 36 | 37 | public function testSendWebmentionHasValidEndpoint() { 38 | $target = 'http://webmention-target.example/has-valid-endpoint.html'; 39 | $response = $this->client->sendWebmention('http://source.example.com/', $target); 40 | $this->assertEquals(202, $response['code']); 41 | } 42 | 43 | public function testSendWebmentionHasErroringEndpoint() { 44 | $target = 'http://webmention-target.example/has-erroring-endpoint.html'; 45 | $response = $this->client->sendWebmention('http://source.example.com/', $target); 46 | $this->assertEquals(500, $response['code']); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /tests/TagParserTest.php: -------------------------------------------------------------------------------- 1 | client = new IndieWeb\MentionClientTest(false, 'empty'); 8 | } 9 | 10 | public function testFindWebmentionTagRelWebmentionHref() { 11 | $html = ''; 12 | $endpoint = $this->client->_findWebmentionEndpointInHTML($html); 13 | $this->assertEquals('http://example.com/webmention', $endpoint); 14 | } 15 | 16 | public function testFindWebmentionATagRelWebmentionHref() { 17 | $html = 'this site supports webmention'; 18 | $endpoint = $this->client->_findWebmentionEndpointInHTML($html); 19 | $this->assertEquals('http://example.com/webmention', $endpoint); 20 | } 21 | 22 | public function testFindWebmentionATagRelSpaceBeforeWebmentionHref() { 23 | $html = 'this site supports webmention'; 24 | $endpoint = $this->client->_findWebmentionEndpointInHTML($html); 25 | $this->assertEquals('http://example.com/webmention', $endpoint); 26 | } 27 | 28 | public function testFindWebmentionATagRelSpaceAfterWebmentionHref() { 29 | $html = 'this site supports webmention'; 30 | $endpoint = $this->client->_findWebmentionEndpointInHTML($html); 31 | $this->assertEquals('http://example.com/webmention', $endpoint); 32 | } 33 | 34 | public function testFindWebmentionLinkTagRelSpaceAroundWebmentionHref() { 35 | $html = ''; 36 | $endpoint = $this->client->_findWebmentionEndpointInHTML($html); 37 | $this->assertEquals('http://example.com/webmention', $endpoint); 38 | } 39 | 40 | public function testFindWebmentionTagHrefRelWebmention() { 41 | $html = ''; 42 | $endpoint = $this->client->_findWebmentionEndpointInHTML($html); 43 | $this->assertEquals('http://example.com/webmention', $endpoint); 44 | } 45 | 46 | public function testFindWebmentionTagRelNoSlashHref() { 47 | $html = ''; 48 | $endpoint = $this->client->_findWebmentionEndpointInHTML($html); 49 | $this->assertEquals('http://example.com/webmention', $endpoint); 50 | } 51 | 52 | public function testFindWebmentionTagHrefRelNoSlash() { 53 | $html = ''; 54 | $endpoint = $this->client->_findWebmentionEndpointInHTML($html); 55 | $this->assertEquals('http://example.com/webmention', $endpoint); 56 | } 57 | 58 | public function testFindWebmentionTagRelHref() { 59 | $html = ''; 60 | $endpoint = $this->client->_findWebmentionEndpointInHTML($html); 61 | $this->assertEquals('http://example.com/webmention', $endpoint); 62 | } 63 | 64 | public function testFindWebmentionTagHrefRel() { 65 | $html = ''; 66 | $endpoint = $this->client->_findWebmentionEndpointInHTML($html); 67 | $this->assertEquals('http://example.com/webmention', $endpoint); 68 | } 69 | 70 | public function testFindWebmentionTagExtraWhitespace() { 71 | $html = ''; 72 | $endpoint = $this->client->_findWebmentionEndpointInHTML($html); 73 | $this->assertEquals('http://example.com/webmention', $endpoint); 74 | } 75 | 76 | public function testFindWebmentionTagNoWhitespace() { 77 | $html = ''; 78 | $endpoint = $this->client->_findWebmentionEndpointInHTML($html); 79 | $this->assertEquals('http://example.com/webmention', $endpoint); 80 | } 81 | 82 | public function testFindWebmentionTagNoCloseTag() { 83 | $html = ''; 84 | $endpoint = $this->client->_findWebmentionEndpointInHTML($html); 85 | $this->assertEquals('http://example.com/webmention', $endpoint); 86 | } 87 | 88 | public function testFindWebmentionTagRelativeUrl() { 89 | $html = ''; 90 | $endpoint = $this->client->_findWebmentionEndpointInHTML($html, 'http://example.com/post/1'); 91 | $this->assertEquals('http://example.com/webmention', $endpoint); 92 | } 93 | 94 | public function testFindWebmentionTagMultipleRels() { 95 | $html = ''; 96 | $endpoint = $this->client->_findWebmentionEndpointInHTML($html, 'http://example.com/post/1'); 97 | $this->assertEquals('http://example.com/webmention', $endpoint); 98 | } 99 | 100 | public function testParseProtocolRelativeURL() { 101 | $html = ''; 102 | $endpoint = $this->client->_findWebmentionEndpointInHTML($html, 'https://example.com/post/1'); 103 | $this->assertEquals('https://example.com/webmention', $endpoint); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/data/pingback-endpoint.example/invalid-body: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Date: Wed, 09 Dec 2015 04:40:18 GMT 3 | Server: Apache 4 | Connection: close 5 | Vary: Accept-Encoding 6 | Content-Type: text/xml; charset=UTF-8 7 | 8 | 9 | 10 | 11 | 12 | 13 | 100 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/data/pingback-endpoint.example/invalid-request: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Date: Wed, 09 Dec 2015 04:40:18 GMT 3 | Server: Apache 4 | Connection: close 5 | Vary: Accept-Encoding 6 | Content-Type: text/xml; charset=UTF-8 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | faultCode 15 | -32700 16 | 17 | 18 | faultString 19 | parse error. not well formed 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/data/pingback-endpoint.example/invalid-xmlrpc: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Date: Wed, 09 Dec 2015 03:29:14 GMT 3 | 4 | This is not an XMLRPC response 5 | -------------------------------------------------------------------------------- /tests/data/pingback-endpoint.example/valid-response: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Date: Wed, 09 Dec 2015 04:40:18 GMT 3 | Server: Apache 4 | Connection: close 5 | Vary: Accept-Encoding 6 | Content-Type: text/xml; charset=UTF-8 7 | 8 | 9 | 10 | 11 | 12 | 13 | pingback was successful 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/data/pingback-target.example/has-erroring-endpoint.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.0.14 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | X-Pingback: http://pingback-endpoint.example/invalid-request 7 | 8 |

Hello World

9 | -------------------------------------------------------------------------------- /tests/data/pingback-target.example/has-valid-endpoint.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.0.14 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | X-Pingback: http://pingback-endpoint.example/valid-response 7 | 8 |

Hello World

9 | -------------------------------------------------------------------------------- /tests/data/pingback-target.example/no-endpoint.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.0.14 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 |

Hello World

8 | -------------------------------------------------------------------------------- /tests/data/source.example.com/mixed-success-links.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | Test 10 | 11 | 12 |

13 | one 14 | two 15 |

16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/data/source.example.com/no-links.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | Test 10 | 11 | 12 |

There are no links in this page

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/source.example.com/send-to-h-entry-links.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | Test 10 | 11 | 12 |
13 |

14 | two 15 |

16 |
17 | one 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/data/source.example.com/target-has-both.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | Test 10 | 11 | 12 | Great post 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/source.example.com/two-valid-links.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | Test 10 | 11 | 12 |

13 | one 14 | two 15 |

16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/data/target.example.com/body-a.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | Test 10 | 11 | 12 |

You can send me a webmention

13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/target.example.com/body-link-org.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

You can send me a webmention

14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/target.example.com/body-link-org2.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

You can send me a webmention

14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/target.example.com/body-link.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

You can send me a webmention or pingback

14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/target.example.com/document-order-1.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | 12 |

You can send me a webmention or pingback

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/target.example.com/document-order-2.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 |

You can send me a webmention or pingback

10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/data/target.example.com/empty-string.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | 12 |

You can send me a webmention

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/target.example.com/false-endpoint-in-comment.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | Test 10 | 11 | 12 |

You can send me a webmention

13 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/data/target.example.com/header.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.0.14 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | Link: ; rel="webmention" 7 | X-Pingback: http://pingback-endpoint.example/valid-response 8 | 9 |

You can send a webmention or a pingback here

10 | -------------------------------------------------------------------------------- /tests/data/target.example.com/only-pingback.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | 12 |

You can send me a pingback

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/target.example.com/pingback-failed.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | 12 |

You can send me a pingback but it will fail

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/target.example.com/redirect.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 302 Found 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | Location: relative/after-redirect.html 7 | 8 | Redirecting... 9 | -------------------------------------------------------------------------------- /tests/data/target.example.com/relative/after-redirect.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.9.10 3 | Content-Type: text/html; charset=UTF-8 4 | Link: ; rel=webmention 5 | 6 | 7 | 8 | 9 | Test 10 | 11 | 12 | No content 13 | 14 | -------------------------------------------------------------------------------- /tests/data/target.example.com/relative/path-relative-endpoint.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.9.10 3 | Content-Type: text/html; charset=UTF-8 4 | Link: ; rel=webmention 5 | 6 | 7 | 8 | 9 | Test 10 | 11 | 12 | No content 13 | 14 | -------------------------------------------------------------------------------- /tests/data/target.example.com/webmention-created.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | 12 |

You can send me a webmention

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/target.example.com/webmention-failed.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

You can send me a webmention or pingback but the webmention will fail

14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/target.example.com/webmention-only-failed.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | 12 |

You can send me a webmention but it will fail

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/target.example.com/webmention-rocks-test-1.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.9.10 3 | Date: Tue, 12 Apr 2016 02:05:15 GMT 4 | Content-Type: text/html; charset=UTF-8 5 | Content-Length: 14459 6 | Connection: keep-alive 7 | X-Powered-By: PHP/5.6.18-1+deb.sury.org~trusty+1 8 | Link: ; rel=webmention 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Webmention Rocks! 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 |
26 | 27 | Webmention Rocks! 28 | 29 |
30 |
31 |

Test #1

32 |
This post advertises its Webmention endpoint with an HTTP Link header. The URL is relative, so this will also test whether your discovery code properly resolves the relative URL.
33 | 39 |
40 |
41 |
42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/data/target.example.com/webmention-rocks-test-2.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.9.10 3 | Date: Tue, 12 Apr 2016 04:18:48 GMT 4 | Content-Type: text/html; charset=UTF-8 5 | Content-Length: 13084 6 | Connection: keep-alive 7 | X-Powered-By: PHP/5.6.18-1+deb.sury.org~trusty+1 8 | Link: ; rel=webmention 9 | 10 | 11 | 12 | 13 | 14 | Webmention Rocks! 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 | Webmention Rocks! 23 | 24 |
25 |
26 |

Test #2

27 |
This post advertises its Webmention endpoint with an HTTP Link header. The Webmention endpoint is listed as an absolute URL.
28 | 34 |
35 |
36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/data/target.example.com/webmention-rocks-test-3.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.9.10 3 | Date: Tue, 12 Apr 2016 04:18:52 GMT 4 | Content-Type: text/html; charset=UTF-8 5 | Content-Length: 13196 6 | Connection: keep-alive 7 | X-Powered-By: PHP/5.6.18-1+deb.sury.org~trusty+1 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Webmention Rocks! 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 | 28 | Webmention Rocks! 29 | 30 |
31 |
32 |

Test #3

33 |
This post advertises its Webmention endpoint with an HTML <link> tag in the document. The URL is relative, so this will also test whether your discovery code properly resolves the relative URL.
34 | 40 |
41 |
42 |
43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /tests/data/target.example.com/webmention-rocks-test-4.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.9.10 3 | Date: Tue, 12 Apr 2016 04:25:44 GMT 4 | Content-Type: text/html; charset=UTF-8 5 | Content-Length: 14981 6 | Connection: keep-alive 7 | X-Powered-By: PHP/5.6.18-1+deb.sury.org~trusty+1 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Webmention Rocks! 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 | 28 | Webmention Rocks! 29 | 30 |
31 |
32 |

Test #4

33 |
This post advertises its Webmention endpoint with an HTML <link> tag in the document. The Webmention endpoint is listed as an absolute URL.
34 | 40 |
41 |
42 |
43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /tests/data/target.example.com/webmention-rocks-test-5.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.9.10 3 | Date: Tue, 12 Apr 2016 04:25:50 GMT 4 | Content-Type: text/html; charset=UTF-8 5 | Content-Length: 13175 6 | Connection: keep-alive 7 | X-Powered-By: PHP/5.6.18-1+deb.sury.org~trusty+1 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Webmention Rocks! 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 | 28 | Webmention Rocks! 29 | 30 |
31 |
32 |

Test #5

33 |
This post advertises its Webmention endpoint with an HTML <a> tag in the body. The URL is relative, so this will also test whether your discovery code properly resolves the relative URL.
34 | 40 |
41 |
42 |
43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /tests/data/target.example.com/webmention-rocks-test-6.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.9.10 3 | Date: Tue, 12 Apr 2016 04:25:54 GMT 4 | Content-Type: text/html; charset=UTF-8 5 | Content-Length: 14937 6 | Connection: keep-alive 7 | X-Powered-By: PHP/5.6.18-1+deb.sury.org~trusty+1 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Webmention Rocks! 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 | 26 | Webmention Rocks! 27 | 28 |
29 |
30 |

Test #6

31 |
This post advertises its Webmention endpoint with an HTML <a> tag in the body. The Webmention endpoint is listed as an absolute URL.
32 | 38 |
39 |
40 |
41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /tests/data/target.example.com/webmention-rocks-test-7.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.9.10 3 | Date: Tue, 12 Apr 2016 04:26:01 GMT 4 | Content-Type: text/html; charset=UTF-8 5 | Content-Length: 13132 6 | Connection: keep-alive 7 | X-Powered-By: PHP/5.6.18-1+deb.sury.org~trusty+1 8 | LinK: ; rel=webmention 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Webmention Rocks! 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 |
26 | 27 | Webmention Rocks! 28 | 29 |
30 |
31 |

Test #7

32 |
This post advertises its Webmention endpoint with an HTTP header with intentionally unusual casing, "LinK". This helps you test whether you are handling HTTP header names in a case insensitive way.
33 | 39 |
40 |
41 |
42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/data/target.example.com/webmention-rocks-test-8.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.9.10 3 | Date: Tue, 12 Apr 2016 04:26:06 GMT 4 | Content-Type: text/html; charset=UTF-8 5 | Content-Length: 13178 6 | Connection: keep-alive 7 | X-Powered-By: PHP/5.6.18-1+deb.sury.org~trusty+1 8 | Link: ; rel="webmention" 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Webmention Rocks! 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 |
26 | 27 | Webmention Rocks! 28 | 29 |
30 |
31 |

Test #8

32 |
This post advertises its Webmention endpoint with an HTTP Link header. Unlike tests #1 and #2, the rel value is quoted, since HTTP allows both rel="webmention" and rel=webmention for the Link header.
33 | 39 |
40 |
41 |
42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/data/webmention-endpoint.example/404-response: -------------------------------------------------------------------------------- 1 | HTTP/1.1 404 Not Found 2 | Date: Wed, 09 Dec 2015 03:29:14 GMT 3 | 4 | Not Found 5 | -------------------------------------------------------------------------------- /tests/data/webmention-endpoint.example/500-response: -------------------------------------------------------------------------------- 1 | HTTP/1.1 500 Internal Server Error 2 | Date: Wed, 09 Dec 2015 03:29:14 GMT 3 | 4 | Something went wrong 5 | -------------------------------------------------------------------------------- /tests/data/webmention-endpoint.example/created-response: -------------------------------------------------------------------------------- 1 | HTTP/1.1 201 Created 2 | Date: Wed, 09 Dec 2015 04:40:18 GMT 3 | Server: Apache 4 | Connection: close 5 | Vary: Accept-Encoding 6 | Location: http://webmention-endpoint.example/status/skldjfosuet 7 | 8 | Webmention processing queued 9 | -------------------------------------------------------------------------------- /tests/data/webmention-endpoint.example/invalid-request: -------------------------------------------------------------------------------- 1 | HTTP/1.1 400 Bad Request 2 | Date: Wed, 09 Dec 2015 04:40:18 GMT 3 | Server: Apache 4 | Connection: close 5 | Vary: Accept-Encoding 6 | 7 | Source or target was invalid 8 | -------------------------------------------------------------------------------- /tests/data/webmention-endpoint.example/queued-response: -------------------------------------------------------------------------------- 1 | HTTP/1.1 202 Accepted 2 | Date: Wed, 09 Dec 2015 04:40:18 GMT 3 | Server: Apache 4 | Connection: close 5 | Vary: Accept-Encoding 6 | 7 | Webmention processing queued 8 | -------------------------------------------------------------------------------- /tests/data/webmention-target.example/has-erroring-endpoint.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.0.14 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Hello World

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/webmention-target.example/has-valid-endpoint.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.0.14 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | Link: ; rel="webmention" 7 | 8 |

Hello World

9 | -------------------------------------------------------------------------------- /tests/data/webmention-target.example/no-endpoint.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/1.0.14 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 |

Hello World

8 | --------------------------------------------------------------------------------