├── public ├── favicon.ico ├── robots.txt ├── images │ └── xkcd.png ├── .htaccess └── index.php ├── certbot └── .gitignore ├── lib ├── .htaccess ├── XRay │ ├── MediaType.php │ ├── Formats │ │ ├── HTMLPurifier_AttrDef_HTML_Microformats2.php │ │ ├── XKCD.php │ │ ├── Hackernews.php │ │ ├── Mf2Feed.php │ │ └── XML.php │ ├── HTTPSig.php │ ├── Rels.php │ └── PostType.php ├── helpers.php └── XRay.php ├── views ├── .htaccess ├── layout.php ├── index.php └── certbot.php ├── controllers ├── .htaccess ├── Main.php ├── Feeds.php └── Rels.php ├── tests ├── data │ ├── .editorconfig │ ├── 404.response.txt │ ├── private.example.com │ │ ├── token-timeout │ │ ├── token-notjson │ │ ├── no-link-headers │ │ ├── token-invalid │ │ ├── token │ │ ├── no-token-endpoint-one-link-header │ │ ├── token-endpoint-timeout │ │ ├── token-endpoint-notjson │ │ ├── token-endpoint-bad-response │ │ ├── token-endpoint │ │ ├── no-token-endpoint-two-link-headers │ │ ├── oauth2-token-endpoint │ │ └── multiple-rels │ ├── source.example.com │ │ ├── deleted-empty │ │ ├── name-no-content │ │ ├── deleted-gone │ │ ├── text-content-with-p-tags │ │ ├── single-h-entry-has-no-permalink │ │ ├── h-x-app │ │ ├── target-test-only-bad-mf1 │ │ ├── deleted │ │ ├── deleted-2 │ │ ├── h-entry-no-content │ │ ├── h-entry-rsvp │ │ ├── rel-alternate-fallback.json │ │ ├── target-test-only-good-mf1 │ │ ├── no-h-entry │ │ ├── h-entry-is-not-first │ │ ├── html-content │ │ ├── html5-tags │ │ ├── link-is-img │ │ ├── link-is-audio │ │ ├── link-is-video │ │ ├── target-test-link-outside-h-entry │ │ ├── target-test-link-outside-valid-mf1 │ │ ├── basictest │ │ ├── person-tag-is-h-card │ │ ├── person-tag-is-url │ │ ├── text-content │ │ ├── content-with-distinct-name │ │ ├── h-entry-with-h-card-before-it │ │ ├── h-entry-with-h-card-sibling │ │ ├── content-with-prefixed-name │ │ ├── multiple-content-type │ │ ├── article-with-featured-image │ │ ├── h-entry-duplicate-categories │ │ ├── multiple-urls-off-domain │ │ ├── h-app │ │ ├── has-syndication │ │ ├── rel-alternate-priority │ │ ├── rel-alternate-as2 │ │ ├── reply-is-url │ │ ├── h-entry-strip-hashtag-from-categories │ │ ├── h-entry-with-two-h-cards-before-it │ │ ├── duplicate-like-of-urls │ │ ├── rel-alternate-not-found │ │ ├── multiple-h-entry-on-permalink │ │ ├── rel-alternate-fallback │ │ ├── duplicate-in-reply-to-urls │ │ ├── rel-alternate-priority-mf2-as2 │ │ ├── h-entry-redirect-with-h-card-sibling │ │ ├── reply-is-h-cite │ │ ├── multiple-urls │ │ ├── fragment-id │ │ ├── checkin-url │ │ ├── h-review-of-h-card │ │ ├── bridgy-follow │ │ ├── follow-of-h-card │ │ ├── rel-alternate-priority.json │ │ ├── h-review-of-product │ │ ├── checkin │ │ ├── hReview │ │ ├── h-event-with-h-card-location │ │ ├── h-event │ │ ├── h-recipe │ │ ├── h-event-text-description │ │ ├── h-event-featured │ │ ├── h-event-text-content │ │ ├── bridgy-invitee │ │ ├── bridgy-example │ │ ├── quotation-of │ │ └── bookmark-missing-content │ ├── sanitize.example │ │ ├── content-is-only-video │ │ ├── photo-in-content-with-p-no-alt │ │ ├── photo-in-content-no-p-with-alt │ │ ├── photo-in-content-with-alt-no-text │ │ ├── photo-in-content-no-p-with-url-photo │ │ ├── photo-with-dupe-name-alt-2 │ │ ├── photo-relative │ │ ├── photo-with-dupe-name-alt │ │ ├── photo-in-content-relative │ │ ├── photo-in-content │ │ ├── photo-in-text-content │ │ ├── html-escaping-in-text │ │ ├── entry-with-p-tags │ │ ├── html-escaping-in-html │ │ ├── photo-in-content-empty-alt │ │ ├── photo-in-content-with-alt │ │ ├── entry-with-br-tags │ │ ├── entry-with-unsafe-tags │ │ ├── entry-with-img-no-implied-photo │ │ ├── h-entry-with-email-author │ │ ├── entry-with-mf2-classes │ │ ├── entry-with-iframe-video │ │ ├── entry-with-valid-tags │ │ ├── h-entry-with-javascript-urls │ │ └── cleverdevil │ ├── redirect.example.com │ │ ├── 0 │ │ ├── 1 │ │ ├── 2 │ │ ├── 3 │ │ ├── code-403 │ │ ├── code-401 │ │ └── code-418 │ ├── feed.example.com │ │ ├── redirect-to-atom │ │ ├── temporary-redirect │ │ ├── permanent-redirect │ │ ├── short-list-of-hentrys-with-h-card │ │ ├── list-of-hentrys │ │ ├── h-feed-author-is-feed │ │ ├── permanent-redirect-target │ │ ├── temporary-redirect-target │ │ ├── h-feed-author-is-bad-feed │ │ ├── html-with-atom-alternate │ │ ├── top-level-h-feed │ │ ├── h-card-with-child-h-entrys │ │ ├── html-with-json-and-atom │ │ ├── h-card-with-sibling-h-entrys │ │ ├── list-of-hentrys-with-h-card │ │ ├── h-feed-with-atom-alternate │ │ ├── h-feed-with-rss-alternate │ │ ├── h-feed-with-child-author │ │ ├── h-card-with-child-h-feed │ │ └── jsonfeed-top-level-author │ ├── author.example.com │ │ ├── h-entry-author-is-name │ │ ├── h-entry-author-is-rel-link-to-h-card-with-rel-me │ │ ├── h-entry-author-is-rel-link-to-h-card-with-url-uid │ │ ├── h-feed-has-multiple-entries-with-different-authors │ │ ├── author-name-is-0 │ │ ├── about-url-uid │ │ ├── about-rel-me │ │ ├── h-entry-author-is-url-to-h-card-with-no-url │ │ ├── about-with-multiple-urls │ │ ├── h-entry-author-is-url-to-h-card-with-multiple-links │ │ ├── about-no-url │ │ ├── about │ │ ├── h-entry-has-h-card-author │ │ ├── h-entry-author-is-rel-link-to-h-card-on-page │ │ ├── h-feed-has-h-card-author │ │ ├── h-entry-author-is-url-to-h-card-on-page │ │ ├── h-entry-has-h-card-and-url-author │ │ ├── h-feed-author │ │ └── h-feed-author-bad │ ├── hacker-news.firebaseio.com │ │ ├── v0_item_27402392.json │ │ ├── v0_item_14516538.json │ │ └── v0_item_14516923.json │ ├── activitystreams.example │ │ ├── note.json │ │ ├── article.json │ │ ├── sensitive.json │ │ ├── reply.json │ │ ├── video.json │ │ ├── photo.json │ │ ├── aaronpk │ │ ├── custom-emoji.json │ │ ├── repost.json │ │ ├── like.json │ │ ├── Gargron │ │ └── jamey │ └── api.github.com │ │ ├── users_sebsel │ │ ├── users_aaronpk │ │ ├── repos_aaronpk_XRay_issues_25 │ │ └── repos_idno_Known_pulls_1690 ├── bootstrap.php ├── FetchTestDisabled.php ├── MediaTypeTest.php └── HelpersTest.php ├── setup ├── login.png ├── challenge-form.png ├── challenge-saved.png ├── indieauth.com.png ├── appengine-enable-cert.png ├── appengine-project-settings.png └── appengine-add-new-certificate.png ├── XRay-67d8fdd0bba3.json.enc ├── php.ini ├── .gitignore ├── .htaccess ├── CONTRIBUTING.md ├── phpunit.xml ├── config.template.php ├── config.production.php ├── index.php ├── app.yaml ├── test-password.php ├── create-password.php ├── release.sh ├── release-library.sh ├── .travis.yml ├── composer.production.json ├── LICENSE.txt ├── composer.json ├── .github └── workflows │ └── php.yml └── TODO.md /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /certbot/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /lib/.htaccess: -------------------------------------------------------------------------------- 1 | deny from all -------------------------------------------------------------------------------- /views/.htaccess: -------------------------------------------------------------------------------- 1 | deny from all -------------------------------------------------------------------------------- /controllers/.htaccess: -------------------------------------------------------------------------------- 1 | deny from all -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /parse 3 | -------------------------------------------------------------------------------- /tests/data/.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = crlf 3 | -------------------------------------------------------------------------------- /setup/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronpk/XRay/HEAD/setup/login.png -------------------------------------------------------------------------------- /public/images/xkcd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronpk/XRay/HEAD/public/images/xkcd.png -------------------------------------------------------------------------------- /tests/data/404.response.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 404 Not Found 2 | 3 | The page was not found. 4 | -------------------------------------------------------------------------------- /setup/challenge-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronpk/XRay/HEAD/setup/challenge-form.png -------------------------------------------------------------------------------- /setup/challenge-saved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronpk/XRay/HEAD/setup/challenge-saved.png -------------------------------------------------------------------------------- /setup/indieauth.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronpk/XRay/HEAD/setup/indieauth.com.png -------------------------------------------------------------------------------- /XRay-67d8fdd0bba3.json.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronpk/XRay/HEAD/XRay-67d8fdd0bba3.json.enc -------------------------------------------------------------------------------- /setup/appengine-enable-cert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronpk/XRay/HEAD/setup/appengine-enable-cert.png -------------------------------------------------------------------------------- /php.ini: -------------------------------------------------------------------------------- 1 | google_app_engine.enable_functions = "php_sapi_name, libxml_disable_entity_loader" 2 | open_basedir = none 3 | -------------------------------------------------------------------------------- /tests/data/private.example.com/token-timeout: -------------------------------------------------------------------------------- 1 | HTTP/1.1 400 Bad Request 2 | X-Test-Error: timeout 3 | 4 | . 5 | -------------------------------------------------------------------------------- /setup/appengine-project-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronpk/XRay/HEAD/setup/appengine-project-settings.png -------------------------------------------------------------------------------- /setup/appengine-add-new-certificate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronpk/XRay/HEAD/setup/appengine-add-new-certificate.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | config.php 3 | vendor/ 4 | php_errors.log 5 | XRay-*.json 6 | .gcloudignore 7 | .phpunit.result.cache 8 | .vscode 9 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteCond %{REQUEST_FILENAME} !-f 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteRule . index.php [L] 5 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteCond %{REQUEST_FILENAME} !-f 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteRule . index.php [L] 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests/ 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/data/source.example.com/deleted-empty: -------------------------------------------------------------------------------- 1 | HTTP/1.1 410 Gone 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 | -------------------------------------------------------------------------------- /config.template.php: -------------------------------------------------------------------------------- 1 | ; rel="micropub" 7 | 8 | This page has no token endpoint specified. 9 | -------------------------------------------------------------------------------- /views/layout.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <?= $this->e($title) ?> 6 | 7 | 8 | 9 | 10 | section('content') ?> 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/data/private.example.com/token-endpoint-timeout: -------------------------------------------------------------------------------- 1 | HTTP/1.1 401 Unauthorized 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/plain; charset=utf-8 5 | Connection: keep-alive 6 | Link: ; rel="token_endpoint" 7 | 8 | This page links to a token endpoint that will time out 9 | -------------------------------------------------------------------------------- /tests/data/private.example.com/token-endpoint-notjson: -------------------------------------------------------------------------------- 1 | HTTP/1.1 401 Unauthorized 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/plain; charset=utf-8 5 | Connection: keep-alive 6 | Link: ; rel="token_endpoint" 7 | 8 | This page links to a token endpoint that does not return JSON 9 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/content-is-only-video: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /tests/data/private.example.com/token-endpoint-bad-response: -------------------------------------------------------------------------------- 1 | HTTP/1.1 401 Unauthorized 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/plain; charset=utf-8 5 | Connection: keep-alive 6 | Link: ; rel="token_endpoint" 7 | 8 | This page links to a token endpoint that will return a bad response 9 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/photo-in-content-with-p-no-alt: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: php72 2 | handlers: 3 | - url: /assets 4 | static_dir: public/assets 5 | - url: /semantic-ui 6 | static_dir: public/semantic-ui 7 | - url: /(favicon\.ico|robots\.txt)$ 8 | static_files: public/\1 9 | upload: public/(favicon\.ico|robots\.txt)$ 10 | - url: .* 11 | script: auto 12 | entrypoint: serve public/index.php 13 | -------------------------------------------------------------------------------- /tests/data/redirect.example.com/code-403: -------------------------------------------------------------------------------- 1 | HTTP/1.1 403 Forbidden 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 | Forbidden 10 | 11 | 12 | Forbidden 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/photo-in-content-no-p-with-alt: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /tests/data/private.example.com/token-endpoint: -------------------------------------------------------------------------------- 1 | HTTP/1.1 401 Unauthorized 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/plain; charset=utf-8 5 | Connection: keep-alive 6 | Link: ; rel="token_endpoint" 7 | 8 | This page uses the "token_endpoint" rel value defined in https://indieweb.org/obtaining-an-access-token 9 | -------------------------------------------------------------------------------- /tests/data/redirect.example.com/code-401: -------------------------------------------------------------------------------- 1 | HTTP/1.1 401 Unauthorized 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 | Unauthorized 10 | 11 | 12 | Unauthorized 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/redirect.example.com/code-418: -------------------------------------------------------------------------------- 1 | HTTP/1.1 418 I'm a Teapot 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 | I'm a Teapot 10 | 11 | 12 | I'm a Teapot 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/photo-in-content-with-alt-no-text: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /tests/data/private.example.com/no-token-endpoint-two-link-headers: -------------------------------------------------------------------------------- 1 | HTTP/1.1 401 Unauthorized 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/plain; charset=utf-8 5 | Connection: keep-alive 6 | Link: ; rel="micropub" 7 | Link: ; rel="webmention" 8 | 9 | This page has no token endpoint specified. 10 | -------------------------------------------------------------------------------- /tests/data/private.example.com/oauth2-token-endpoint: -------------------------------------------------------------------------------- 1 | HTTP/1.1 401 Unauthorized 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/plain; charset=utf-8 5 | Connection: keep-alive 6 | Link: ; rel="oauth2-token" 7 | 8 | This page uses the "oauth2-token" rel value defined in https://tools.ietf.org/html/draft-wmills-oauth-lrdd-07 9 | -------------------------------------------------------------------------------- /controllers/Main.php: -------------------------------------------------------------------------------- 1 | setContent(p3k\XRay\view('index', [ 9 | 'title' => 'X-Ray' 10 | ])); 11 | return $response; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /tests/data/source.example.com/name-no-content: -------------------------------------------------------------------------------- 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 |

Hello World

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/redirect.example.com/1: -------------------------------------------------------------------------------- 1 | HTTP/1.1 301 Moved Permanently 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: http://redirect.example.com/0 7 | 8 | 9 | 10 | Moved 11 | 12 | 13 | This page has moved 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/redirect.example.com/2: -------------------------------------------------------------------------------- 1 | HTTP/1.1 301 Moved Permanently 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: http://redirect.example.com/1 7 | 8 | 9 | 10 | Moved 11 | 12 | 13 | This page has moved 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/redirect.example.com/3: -------------------------------------------------------------------------------- 1 | HTTP/1.1 301 Moved Permanently 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: http://redirect.example.com/2 7 | 8 | 9 | 10 | Moved 11 | 12 | 13 | This page has moved 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/source.example.com/deleted-gone: -------------------------------------------------------------------------------- 1 | HTTP/1.1 410 Gone 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 |

This post has been deleted.

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/redirect-to-atom: -------------------------------------------------------------------------------- 1 | HTTP/1.1 301 Moved Permanently 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: http://feed.example.com/atom 7 | 8 | 9 | 10 | Moved 11 | 12 | 13 | This page has moved 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/private.example.com/multiple-rels: -------------------------------------------------------------------------------- 1 | HTTP/1.1 401 Unauthorized 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/plain; charset=utf-8 5 | Connection: keep-alive 6 | Link: ; rel="token_endpoint" 7 | Link: ; rel="webmention" 8 | 9 | This page uses the "token_endpoint" rel value defined in https://indieweb.org/obtaining-an-access-token 10 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/photo-in-content-no-p-with-url-photo: -------------------------------------------------------------------------------- 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 | permalink 12 |
13 | -------------------------------------------------------------------------------- /tests/data/source.example.com/text-content-with-p-tags: -------------------------------------------------------------------------------- 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 |

Hello

World

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/temporary-redirect: -------------------------------------------------------------------------------- 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: http://feed.example.com/temporary-redirect-target 7 | 8 | 9 | 10 | Moved 11 | 12 | 13 | This page has moved 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/permanent-redirect: -------------------------------------------------------------------------------- 1 | HTTP/1.1 301 Moved Permanently 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: http://feed.example.com/permanent-redirect-target 7 | 8 | 9 | 10 | Moved 11 | 12 | 13 | This page has moved 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/source.example.com/single-h-entry-has-no-permalink: -------------------------------------------------------------------------------- 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 |

Hello World

14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-x-app: -------------------------------------------------------------------------------- 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 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/source.example.com/target-test-only-bad-mf1: -------------------------------------------------------------------------------- 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 |

target

14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/data/source.example.com/deleted: -------------------------------------------------------------------------------- 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 |

This post has been deleted.

14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/source.example.com/deleted-2: -------------------------------------------------------------------------------- 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 |

This post has been deleted.

14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-entry-no-content: -------------------------------------------------------------------------------- 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 |

This is a Post

13 | permalink 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/photo-with-dupe-name-alt-2: -------------------------------------------------------------------------------- 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 |

Photo caption Photo caption

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-entry-rsvp: -------------------------------------------------------------------------------- 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 | I'll be there! 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/data/source.example.com/rel-alternate-fallback.json: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: application/mf2+json 5 | Connection: keep-alive 6 | 7 | { 8 | "type": [ 9 | "h-entry" 10 | ], 11 | "properties": { 12 | "content": [ 13 | "XRay should not use this content since the JSON must be an entire page parsed result" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/data/source.example.com/target-test-only-good-mf1: -------------------------------------------------------------------------------- 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 |

target

14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/data/source.example.com/no-h-entry: -------------------------------------------------------------------------------- 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 |

This page has a link to target.example.com and some formatted text but has no h-entry markup.

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/redirect.example.com/0: -------------------------------------------------------------------------------- 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 | The Final Page 10 | 11 | 12 |

The Final Page

13 |

This is the final page.

14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/photo-relative: -------------------------------------------------------------------------------- 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 |

Test of relative URL resolution with an photo property

13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-entry-is-not-first: -------------------------------------------------------------------------------- 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 | 2016 13 |
14 |

Hello World

15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/photo-with-dupe-name-alt: -------------------------------------------------------------------------------- 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 |

Photo caption

13 |

Photo caption

14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/source.example.com/html-content: -------------------------------------------------------------------------------- 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 |

This page has a link to target.example.com and some formatted text.

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/source.example.com/html5-tags: -------------------------------------------------------------------------------- 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 |

Hello World 14 |

The content of the blog post
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/data/source.example.com/link-is-img: -------------------------------------------------------------------------------- 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 |

This page has an img tag with the target URL.

13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/author.example.com/h-entry-author-is-name: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 |
Author Name
16 |

Hello World

17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/photo-in-content-relative: -------------------------------------------------------------------------------- 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 |

Test of relative URL resolution with two images inside the content

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/source.example.com/link-is-audio: -------------------------------------------------------------------------------- 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 |

This page has an audio tag with the target URL.

13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/source.example.com/link-is-video: -------------------------------------------------------------------------------- 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 |

This page has a video tag with the target URL.

13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/source.example.com/target-test-link-outside-h-entry: -------------------------------------------------------------------------------- 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 |

hello world

14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/photo-in-content: -------------------------------------------------------------------------------- 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 |

This is a photo post with an img tag inside the content.

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/source.example.com/target-test-link-outside-valid-mf1: -------------------------------------------------------------------------------- 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 |

hello world

14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/photo-in-text-content: -------------------------------------------------------------------------------- 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 |

This is a photo post with an img tag inside the content.

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/source.example.com/basictest: -------------------------------------------------------------------------------- 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 |

This page has links to target.example.com and target2.example.com.

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/html-escaping-in-text: -------------------------------------------------------------------------------- 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 |

This content has some HTML escaped entities such as & ampersand, " quote, escaped <code> HTML tags, an ümlaut, an @at sign.

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/source.example.com/person-tag-is-h-card: -------------------------------------------------------------------------------- 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 |

Hello World

13 |
14 | Alice 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/data/source.example.com/person-tag-is-url: -------------------------------------------------------------------------------- 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 |

Hello World

13 | Alice 14 | permalink 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/entry-with-p-tags: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 02 Mar 2018 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 |

This is a multiline post separated by paragraph tags with no space between them.

This is how Mastodon formats HTML.

14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/html-escaping-in-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 |

This content has some HTML escaped entities such as & ampersand, " quote, escaped <code> HTML tags, an ümlaut, an @at sign.

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/photo-in-content-empty-alt: -------------------------------------------------------------------------------- 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 |

This is a photo post with an img tag inside the content.

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/source.example.com/text-content: -------------------------------------------------------------------------------- 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 |

This page has a link to target.example.com and some formatted text but is in a p-content element so is plaintext.

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/photo-in-content-with-alt: -------------------------------------------------------------------------------- 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 |

This is a photo post with an img tag inside the content. a photo

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/author.example.com/h-entry-author-is-rel-link-to-h-card-with-rel-me: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 |

Hello World

16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/data/author.example.com/h-entry-author-is-rel-link-to-h-card-with-url-uid: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 |

Hello World

16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/data/author.example.com/h-feed-has-multiple-entries-with-different-authors: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 | 16 |
17 |

Hello World

18 |
19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/entry-with-br-tags: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 02 Mar 2018 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 | This content has two break tags to indicate a paragraph break.

This is how tantek's autolinker works. 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/data/source.example.com/content-with-distinct-name: -------------------------------------------------------------------------------- 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 |

Hello World

13 |

This page has a link to target.example.com and some formatted text.

14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/author.example.com/author-name-is-0: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Author 11 | 12 | 13 | 14 |
15 | 16 | 17 | 0 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-entry-with-h-card-before-it: -------------------------------------------------------------------------------- 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 | Author Name 13 |
14 |

Hello World

15 | permalink 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-entry-with-h-card-sibling: -------------------------------------------------------------------------------- 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 |

Hello World

14 | permalink 15 |
16 | Author Name 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/data/source.example.com/content-with-prefixed-name: -------------------------------------------------------------------------------- 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 |

This page has a link...

13 |

This page has a link to target.example.com and some formatted text.

14 | 15 | 16 | -------------------------------------------------------------------------------- /test-password.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | Author 11 | 12 | 13 | 14 |
15 | 16 | 17 | Author Full Name 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/data/source.example.com/multiple-content-type: -------------------------------------------------------------------------------- 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 | Content-Type: text/html 6 | Connection: keep-alive 7 | 8 | 9 | 10 | Test 11 | 12 | 13 |

This page has a link to target.example.com and some formatted text but is in a p-content element so is plaintext.

14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/source.example.com/article-with-featured-image: -------------------------------------------------------------------------------- 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 |

Post Title

15 |

This is a blog post.

16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-entry-duplicate-categories: -------------------------------------------------------------------------------- 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 |

This page is an h-entry and has some duplicate categories #indieweb.

13 | 14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/data/author.example.com/about-rel-me: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Author 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | Author Full Name 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/data/source.example.com/multiple-urls-off-domain: -------------------------------------------------------------------------------- 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 |

Homebrew Website Club!

13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-app: -------------------------------------------------------------------------------- 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 | 15 | Quill 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/entry-with-unsafe-tags: -------------------------------------------------------------------------------- 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 |

This content has a bunch of invalid tags but also some valid ones.

14 | 15 | 16 |

Hello World

17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/data/source.example.com/has-syndication: -------------------------------------------------------------------------------- 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 |

This page has links to target.example.com and target2.example.com.

13 | syndicated post 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/source.example.com/rel-alternate-priority: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | Test 12 | 13 | 14 | 15 |
16 |

This is the content in the HTML page

17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /create-password.php: -------------------------------------------------------------------------------- 1 | 8 | 9 | Test 10 | 11 | 12 | 13 | 19 | 20 | Author Name 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/data/hacker-news.firebaseio.com/v0_item_27402392.json: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx 3 | Date: Fri, 09 Jun 2017 14:30:19 GMT 4 | Content-Type: application/json; charset=utf-8 5 | Content-Length: 1701 6 | Connection: keep-alive 7 | Access-Control-Allow-Origin: * 8 | Cache-Control: no-cache 9 | Strict-Transport-Security: max-age=31556926; includeSubDomains; preload 10 | 11 | {"by":"giuliomagnifico","descendants":0,"id":27402392,"score":2,"time":1622869705,"title":"The Impossibility of Perfectly Caching HTTP Range Requests","type":"story","url":"https://kevincox.ca/2021/06/04/http-range-caching/"} -------------------------------------------------------------------------------- /tests/data/source.example.com/rel-alternate-as2: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | Test 12 | 13 | 14 | 15 |
16 |

This is the content in the HTML instead of the AS2 JSON

17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/data/author.example.com/h-entry-author-is-url-to-h-card-with-no-url: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 | Author 16 |

Hello World

17 | permalink 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/data/source.example.com/reply-is-url: -------------------------------------------------------------------------------- 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 | in reply to 13 |

This page has a link to target.example.com and some formatted text.

14 | permalink 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-entry-strip-hashtag-from-categories: -------------------------------------------------------------------------------- 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 |

This page is an h-entry and has some duplicate categories #indieweb.

13 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-entry-with-two-h-cards-before-it: -------------------------------------------------------------------------------- 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 | Author Name 13 | Author Org 14 |
15 |

Hello World

16 | permalink 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/data/source.example.com/duplicate-like-of-urls: -------------------------------------------------------------------------------- 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 | liked 13 | a post 14 |
15 | this post 16 |
17 | permalink 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/data/source.example.com/rel-alternate-not-found: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | Test 12 | 13 | 14 | 15 | 16 |
17 |

Test content with a rel alternate link to a 404 page

18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/entry-with-img-no-implied-photo: -------------------------------------------------------------------------------- 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 |

This is a photo post with an img tag inside the content, which does not have a u-photo class so should not be removed. a photo

13 | permalink 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/data/source.example.com/multiple-h-entry-on-permalink: -------------------------------------------------------------------------------- 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 |

Primary Post

14 | permalink 15 |
16 |
17 |

Next Post

18 | read more 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | release_dir=../XRay-release 4 | 5 | current=`pwd` 6 | 7 | composer install 8 | 9 | rm $release_dir/xray-app.zip 10 | rsync -ap --delete controllers $release_dir/ 11 | rsync -ap --delete lib $release_dir/ 12 | rsync -ap --delete public $release_dir/ 13 | rsync -ap --delete views $release_dir/ 14 | rsync -ap --delete --exclude=.git vendor $release_dir/ 15 | cp README.md $release_dir/ 16 | cp LICENSE.txt $release_dir/ 17 | cp index.php $release_dir/ 18 | cp .htaccess $release_dir/ 19 | cp controllers/.htaccess $release_dir/vendor/ 20 | 21 | cd $release_dir 22 | zip -r xray-app.zip . 23 | 24 | cd $current 25 | -------------------------------------------------------------------------------- /tests/data/author.example.com/about-with-multiple-urls: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Author 11 | 12 | 13 | 14 |
15 |

Author Full Name

16 | xmpp 17 | Me 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/data/author.example.com/h-entry-author-is-url-to-h-card-with-multiple-links: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 | Author 16 |

Hello World

17 | permalink 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/data/source.example.com/rel-alternate-fallback: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | Test 12 | 13 | 14 | 15 |
16 |

XRay should use this content since the JSON in the rel-alternate is invalid

17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/data/author.example.com/about-no-url: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Author 11 | 12 | 13 | 14 |
15 |

Author Full Name

16 | xmpp 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /release-library.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | release_dir=../XRay-library-release 4 | 5 | current=`pwd` 6 | 7 | composer install --no-dev 8 | 9 | rm $release_dir/xray-library.zip 10 | rsync -ap --delete controllers $release_dir/ 11 | rsync -ap --delete lib $release_dir/ 12 | rsync -ap --delete public $release_dir/ 13 | rsync -ap --delete views $release_dir/ 14 | rsync -ap --delete --exclude=.git vendor $release_dir/ 15 | cp README.md $release_dir/ 16 | cp LICENSE.txt $release_dir/ 17 | cp index.php $release_dir/ 18 | cp .htaccess $release_dir/ 19 | cp controllers/.htaccess $release_dir/vendor/ 20 | 21 | cd $release_dir 22 | zip -r xray-library.zip . 23 | 24 | cd $current 25 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | Author 11 | 12 | 13 | 14 |
15 | 16 | 17 | Author Full Name 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/data/author.example.com/h-entry-has-h-card-author: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 | Author 20 | 21 |
22 | 23 |

Hello World

24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/h-entry-with-email-author: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 | Author 20 | 21 |
22 | 23 |

Hello World

24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/data/source.example.com/duplicate-in-reply-to-urls: -------------------------------------------------------------------------------- 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 | in reply to 13 |
14 | this post 15 |
16 |

This page has duplicate in-reply-to values.

17 | permalink 18 | 19 | 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.6 4 | - 7.0 5 | - 7.1 6 | - 7.2 7 | - 7.3 8 | sudo: false 9 | before_script: 10 | - composer self-update 11 | - composer install --prefer-dist --dev --no-interaction 12 | before_install: 13 | - cp config.production.php config.php 14 | - cp composer.production.json composer.json 15 | - rm composer.lock 16 | before_deploy: 17 | - openssl aes-256-cbc -K $encrypted_e44c58426490_key -iv $encrypted_e44c58426490_iv 18 | -in XRay-67d8fdd0bba3.json.enc -out XRay-67d8fdd0bba3.json -d 19 | deploy: 20 | skip_cleanup: true 21 | provider: gae 22 | default: true 23 | project: xray-p3k-io 24 | on: 25 | branch: main 26 | php: 7.2 27 | keyfile: XRay-67d8fdd0bba3.json 28 | -------------------------------------------------------------------------------- /tests/data/author.example.com/h-entry-author-is-rel-link-to-h-card-on-page: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 |

Hello World

16 |
17 | 18 |
19 | 20 | 21 | Author 22 | 23 |
24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/entry-with-mf2-classes: -------------------------------------------------------------------------------- 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 |

Hello World

14 | This content has a valid tags that contains microformats classes. 15 |

Utility Class

16 | The u-utilityClass should not be returned according to http://microformats.org/wiki/microformats2-parsing-issues#ignore_u-camelCase_properties 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/data/source.example.com/rel-alternate-priority-mf2-as2: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: text/html 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | Test 12 | 13 | 14 | 15 | 16 |
17 |

This should not be the content from XRay

18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/data/author.example.com/h-feed-has-h-card-author: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 | Author 20 | 21 |
22 | 23 |
24 |

Hello World

25 |
26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-entry-redirect-with-h-card-sibling: -------------------------------------------------------------------------------- 1 | HTTP/1.1 301 Moved Permanently 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: /h-entry-with-h-card-sibling 7 | 8 | 9 | 10 | Test 11 | 12 | 13 |
14 |

Hello World

15 | permalink 16 | 17 |
18 | Author Name 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/data/source.example.com/reply-is-h-cite: -------------------------------------------------------------------------------- 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 | Example Post 14 | Author 15 | permalink 16 |
17 |

This page has a link to target.example.com and some formatted text.

18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/data/source.example.com/multiple-urls: -------------------------------------------------------------------------------- 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 |

Homebrew Website Club!

13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/data/source.example.com/fragment-id: -------------------------------------------------------------------------------- 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 |

This page has comments.

13 | 20 | permalink 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/list-of-hentrys: -------------------------------------------------------------------------------- 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 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/h-feed-author-is-feed: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 | 16 |
17 |

Hello World

18 | 19 |
20 | 21 |
22 |

Hello World

23 | 24 |
25 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/permanent-redirect-target: -------------------------------------------------------------------------------- 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 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/temporary-redirect-target: -------------------------------------------------------------------------------- 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 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/h-feed-author-is-bad-feed: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 | 16 |
17 |

Hello World

18 | 19 |
20 | 21 |
22 |

Hello World

23 | 24 |
25 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/data/author.example.com/h-entry-author-is-url-to-h-card-on-page: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 |

Hello World

16 | Author 17 | permalink 18 |
19 | 20 |
21 | 22 | 23 | Author 24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/data/source.example.com/checkin-url: -------------------------------------------------------------------------------- 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 | at DreamHost 14 | 15 |

Homebrew Website Club!

16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-review-of-h-card: -------------------------------------------------------------------------------- 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 | Review 10 | 11 | 12 |

Review

13 | 14 | permalink 15 | 16 |

The Reviewed Business

17 | 18 | 3 out of 5 19 | 20 |
Not great
21 | 22 |
23 | This is the full text of the review 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/data/author.example.com/h-entry-has-h-card-and-url-author: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 | 16 | Author 17 | 18 |
19 | 20 | 21 | Author 22 | 23 |
24 | 25 |

Hello World

26 | 27 | permalink 28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/data/author.example.com/h-feed-author: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 | Author 20 | 21 |
22 | 23 |
24 |

Hello World

25 |
26 | 27 |
28 |

Hello World

29 |
30 | 31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/data/activitystreams.example/note.json: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 30 Jul 2018 03:29:14 GMT 4 | Content-Type: application/activity+json 5 | Connection: keep-alive 6 | 7 | { 8 | "@context": "https://www.w3.org/ns/activitystreams", 9 | "id": "http://activitystreams.example/note.json", 10 | "type": "Note", 11 | "published": "2018-07-12T13:02:04-07:00", 12 | "attributedTo": "https://activitystreams.example/aaronpk", 13 | "content": "This is the text content of an ActivityStreams note", 14 | "to": [ 15 | "https://www.w3.org/ns/activitystreams#Public" 16 | ], 17 | "tag": [ 18 | { 19 | "id": "https://aaronparecki.com/tag/activitystreams", 20 | "name": "#activitystreams", 21 | "type": "Hashtag" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/data/author.example.com/h-feed-author-bad: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 | Author 20 | 21 |
22 | 23 |
24 |

Hello World

25 |
26 | 27 |
28 |

Hello World

29 |
30 | 31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/entry-with-iframe-video: -------------------------------------------------------------------------------- 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 |

This is a nice video!

14 | 15 |

This is a not a nice video!

16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/data/activitystreams.example/article.json: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 30 Jul 2018 03:29:14 GMT 4 | Content-Type: application/activity+json 5 | Connection: keep-alive 6 | 7 | { 8 | "@context": "https://www.w3.org/ns/activitystreams", 9 | "id": "http://activitystreams.example/article.json", 10 | "type": "Article", 11 | "published": "2018-07-12T13:02:04-07:00", 12 | "attributedTo": "https://activitystreams.example/aaronpk", 13 | "name": "An Article", 14 | "content": "

This is the content of an ActivityStreams article

", 15 | "to": [ 16 | "https://www.w3.org/ns/activitystreams#Public" 17 | ], 18 | "tag": [ 19 | { 20 | "id": "https://aaronparecki.com/tag/activitystreams", 21 | "name": "#activitystreams", 22 | "type": "Hashtag" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/html-with-atom-alternate: -------------------------------------------------------------------------------- 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 |

Author Name

15 | 16 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/data/source.example.com/bridgy-follow: -------------------------------------------------------------------------------- 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 | https://mastodon.social/b8177a60-8f80-47a0-b943-a9df876cf5d5 13 | 14 | 15 | 16 | 17 | 18 | https://mastodon.social/@swentel 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/top-level-h-feed: -------------------------------------------------------------------------------- 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 | Author Name 15 | 16 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/data/activitystreams.example/sensitive.json: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 30 Jul 2018 03:29:14 GMT 4 | Content-Type: application/activity+json 5 | Connection: keep-alive 6 | 7 | { 8 | "@context": "https://www.w3.org/ns/activitystreams", 9 | "id": "http://activitystreams.example/sensitive.json", 10 | "type": "Note", 11 | "published": "2018-07-12T13:02:04-07:00", 12 | "attributedTo": "https://activitystreams.example/aaronpk", 13 | "sensitive": true, 14 | "summary": "sensitive topic", 15 | "content": "This is the text content of a sensitive ActivityStreams note", 16 | "to": [ 17 | "https://www.w3.org/ns/activitystreams#Public" 18 | ], 19 | "tag": [ 20 | { 21 | "id": "https://aaronparecki.com/tag/activitystreams", 22 | "name": "#activitystreams", 23 | "type": "Hashtag" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tests/data/activitystreams.example/reply.json: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 30 Jul 2018 03:29:14 GMT 4 | Content-Type: application/activity+json 5 | Connection: keep-alive 6 | 7 | { 8 | "@context": "https://www.w3.org/ns/activitystreams", 9 | "id": "http://activitystreams.example/reply.json", 10 | "type": "Note", 11 | "published": "2018-07-12T13:02:04-07:00", 12 | "attributedTo": "https://activitystreams.example/aaronpk", 13 | "content": "@aaronpk This is a reply", 14 | "inReplyTo": "http://activitystreams.example/note.json", 15 | "to": [ 16 | "https://www.w3.org/ns/activitystreams#Public" 17 | ], 18 | "tag": [ 19 | { 20 | "type": "Mention", 21 | "href": "http://activitystreams.example/aaronpk", 22 | "name": "@aaronpk@activitystreams.example" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tests/data/source.example.com/follow-of-h-card: -------------------------------------------------------------------------------- 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 | https://mastodon.social/b8177a60-8f80-47a0-b943-a9df876cf5d5 13 | 14 | 15 | 16 | 17 | 18 | https://mastodon.social/@swentel 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | swentel 34 | 35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/data/source.example.com/rel-alternate-priority.json: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 03:29:14 GMT 4 | Content-Type: application/mf2+json 5 | Connection: keep-alive 6 | 7 | { 8 | "items": [ 9 | { 10 | "type": [ 11 | "h-entry" 12 | ], 13 | "properties": { 14 | "content": [ 15 | "This is the content in the MF2 JSON file" 16 | ] 17 | } 18 | } 19 | ], 20 | "rels": { 21 | "alternate": [ 22 | "http://source.example.com/rel-alternate-priority.json" 23 | ] 24 | }, 25 | "rel-urls": { 26 | "http://source.example.com/rel-alternate-priority.json": { 27 | "type": "application/mf2+json", 28 | "rels": [ 29 | "alternate" 30 | ] 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/h-card-with-child-h-entrys: -------------------------------------------------------------------------------- 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 | Author Name 15 | 16 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-review-of-product: -------------------------------------------------------------------------------- 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 | Review 10 | 11 | 12 |

Review

13 | 14 | permalink 15 | 16 |

The Reviewed Product

17 | 18 | 3 out of 5 19 | 20 |
Not great
21 | 22 |
23 | This is the full text of the review 24 |
25 | 26 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/html-with-json-and-atom: -------------------------------------------------------------------------------- 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 | 15 |

Author Name

16 | 17 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/data/source.example.com/checkin: -------------------------------------------------------------------------------- 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 | at DreamHost 14 |
15 | 45.518716 16 | -122.679614 17 |
18 |
19 |

Homebrew Website Club!

20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/data/source.example.com/hReview: -------------------------------------------------------------------------------- 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 | Review 10 | 11 | 12 |

Review

13 | 14 | 15 | 16 |
17 |

The Reviewed Product

18 |
19 | 20 | 3 out of 5 21 | 22 | Aaron Parecki 23 | 24 | 2016-12-15T22:32:42+01:00 25 | 26 |
Not great
27 | 28 |
29 | This is the full text of the review 30 |
31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/h-card-with-sibling-h-entrys: -------------------------------------------------------------------------------- 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 | Author Name 14 | 15 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/list-of-hentrys-with-h-card: -------------------------------------------------------------------------------- 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 | 31 | 32 | Author Name 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/data/activitystreams.example/video.json: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 30 Jul 2018 03:29:14 GMT 4 | Content-Type: application/activity+json 5 | Connection: keep-alive 6 | 7 | { 8 | "@context": "https://www.w3.org/ns/activitystreams", 9 | "id": "http://activitystreams.example/video.json", 10 | "type": "Note", 11 | "published": "2018-07-12T13:02:04-07:00", 12 | "attributedTo": "https://activitystreams.example/aaronpk", 13 | "content": "This is the text content of an ActivityStreams photo", 14 | "to": [ 15 | "https://www.w3.org/ns/activitystreams#Public" 16 | ], 17 | "tag": [ 18 | { 19 | "id": "https://aaronparecki.com/tag/activitystreams", 20 | "name": "#activitystreams" 21 | } 22 | ], 23 | "attachment": [ 24 | { 25 | "type": "Document", 26 | "mediaType": "video/mp4", 27 | "url": "https://aaronparecki.com/2018/07/21/19/video.mp4", 28 | "name": null 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-event-with-h-card-location: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 09 Dec 2015 02:29:12 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | Homebrew Website Club 10 | 11 | 12 |

Homebrew Website Club

13 | 14 | permalink 15 | 16 | - 17 | 18 | 19 | Venue 20 | 45.5 21 | -122.6 22 | 1234 Main St 23 | Portland 24 | Oregon 25 | USA 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /composer.production.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "p3k/xray", 3 | "type": "library", 4 | "license": "MIT", 5 | "homepage": "https://github.com/aaronpk/XRay", 6 | "description": "X-Ray returns structured data from any URL", 7 | "require": { 8 | "league/plates": "3.*", 9 | "league/route": "1.*", 10 | "mf2/mf2": "^0.4", 11 | "ezyang/htmlpurifier": "4.10.*", 12 | "indieweb/link-rel-parser": "0.1.*", 13 | "dg/twitter-php": "3.6.*", 14 | "p3k/timezone": "*", 15 | "p3k/http": ">=0.1.8", 16 | "cebe/markdown": "1.1.*", 17 | "p3k/picofeed": ">=0.1.35", 18 | "masterminds/html5": "^2.3" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "p3k\\XRay\\": "lib/XRay" 23 | }, 24 | "files": [ 25 | "lib/helpers.php", 26 | "lib/XRay.php", 27 | "controllers/Main.php", 28 | "controllers/Parse.php", 29 | "controllers/Token.php", 30 | "controllers/Rels.php", 31 | "controllers/Feeds.php", 32 | "controllers/Certbot.php" 33 | ] 34 | }, 35 | "require-dev": { 36 | "phpunit/phpunit": "4.8.*" 37 | } 38 | } -------------------------------------------------------------------------------- /tests/data/activitystreams.example/photo.json: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 30 Jul 2018 03:29:14 GMT 4 | Content-Type: application/activity+json 5 | Connection: keep-alive 6 | 7 | { 8 | "@context": "https://www.w3.org/ns/activitystreams", 9 | "id": "http://activitystreams.example/photo.json", 10 | "type": "Note", 11 | "published": "2018-07-12T13:02:04-07:00", 12 | "attributedTo": "https://activitystreams.example/aaronpk", 13 | "content": "This is the text content of an ActivityStreams photo", 14 | "to": [ 15 | "https://www.w3.org/ns/activitystreams#Public" 16 | ], 17 | "tag": [ 18 | { 19 | "type": "Hashtag", 20 | "id": "https://aaronparecki.com/tag/activitystreams", 21 | "name": "#activitystreams" 22 | } 23 | ], 24 | "attachment": [ 25 | { 26 | "type": "Image", 27 | "mediaType": "image/jpeg", 28 | "url": "https://aaronparecki.com/2018/06/28/26/photo.jpg", 29 | "name": null 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tests/FetchTestDisabled.php: -------------------------------------------------------------------------------- 1 | http = new p3k\HTTP(); 13 | } 14 | 15 | public function testTimeout() 16 | { 17 | $url = 'https://nghttp2.org/httpbin/delay/2'; 18 | $this->http->timeout = 1; 19 | $response = $this->http->get($url); 20 | $this->assertEquals('timeout', $response['error']); 21 | } 22 | 23 | public function testRedirectLimit() 24 | { 25 | $url = 'https://nghttp2.org/httpbin/redirect/3'; 26 | $this->http->max_redirects = 1; 27 | $response = $this->http->get($url); 28 | $this->assertEquals('too_many_redirects', $response['error']); 29 | } 30 | 31 | public function testNoError() 32 | { 33 | $url = 'https://nghttp2.org/httpbin/ip'; 34 | $response = $this->http->get($url); 35 | $this->assertEquals('', $response['error']); 36 | } 37 | } -------------------------------------------------------------------------------- /tests/data/feed.example.com/h-feed-with-atom-alternate: -------------------------------------------------------------------------------- 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 | Author Name 15 | 16 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/h-feed-with-rss-alternate: -------------------------------------------------------------------------------- 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 | Author Name 15 | 16 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Aaron Parecki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/XRay/MediaType.php: -------------------------------------------------------------------------------- 1 | type: application, subtype: json, format: json 13 | // "application/ld+json" => type: application, subtype: "ld+json", format: json 14 | public function __construct($string) { 15 | if(strstr($string, ';')) { 16 | list($type, $parameters) = explode(';', $string, 2); 17 | 18 | $parameters = explode(';', $parameters); 19 | foreach($parameters as $p) { 20 | list($k, $v) = explode('=', trim($p)); 21 | if($k == 'charset') 22 | $this->charset = $v; 23 | } 24 | } else { 25 | $type = $string; 26 | } 27 | 28 | list($type, $subtype) = explode('/', $type); 29 | 30 | $this->type = $type; 31 | $this->subtype = $subtype; 32 | $this->format = $subtype; 33 | 34 | if(strstr($subtype, '+')) { 35 | list($a, $b) = explode('+', $subtype, 2); 36 | $this->format = $b; 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "p3k/xray", 3 | "type": "library", 4 | "license": "MIT", 5 | "homepage": "https://github.com/aaronpk/XRay", 6 | "description": "X-Ray returns structured data from any URL", 7 | "require": { 8 | "mf2/mf2": "^0.4||^0.5", 9 | "ezyang/htmlpurifier": "^4.10", 10 | "indieweb/link-rel-parser": "^0.1.0", 11 | "p3k/timezone": "^0.1.0", 12 | "p3k/http": "^0.1.0", 13 | "cebe/markdown": "^1.1.0", 14 | "p3k/picofeed": "^0.1.0", 15 | "masterminds/html5": "^2.3" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "p3k\\XRay\\": "lib/XRay" 20 | }, 21 | "files": [ 22 | "lib/helpers.php", 23 | "lib/XRay.php" 24 | ] 25 | }, 26 | "require-dev": { 27 | "league/plates": "^3.0", 28 | "league/route": "^1.0", 29 | "phpunit/phpunit": "^8.0.0|^9.0.0" 30 | }, 31 | "autoload-dev": { 32 | "files": [ 33 | "controllers/Main.php", 34 | "controllers/Parse.php", 35 | "controllers/Token.php", 36 | "controllers/Rels.php", 37 | "controllers/Feeds.php", 38 | "controllers/Certbot.php" 39 | ] 40 | }, 41 | "scripts": { 42 | "test": "vendor/bin/phpunit" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-event: -------------------------------------------------------------------------------- 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 | Homebrew Website Club 10 | 11 | 12 |

Homebrew Website Club

13 | 14 | permalink 15 | 16 | - 17 | 18 |
19 |

Are you building your own website? Indie reader? Personal publishing web app? Or some other digital magic-cloud proxy? If so, come on by and join a gathering of people with likeminded interests. Bring your friends that want to start a personal web site. Exchange information, swap ideas, talk shop, help work on a project...

20 |

See the Homebrew Website Club Newsletter Volume 1 Issue 1 for a description of the first meeting.

21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-recipe: -------------------------------------------------------------------------------- 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 | Cookies 10 | 11 | 12 |

Cookie Recipe

13 | 14 | permalink 15 | 16 |
12 Cookies
17 | 18 | 19 | 20 |
The best chocolate chip cookie recipe
21 | 22 |

Ingredients

23 | 29 | 30 |

Instructions

31 |
32 | 37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-event-text-description: -------------------------------------------------------------------------------- 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 | Homebrew Website Club 10 | 11 | 12 |

Homebrew Website Club

13 | 14 | permalink 15 | 16 | - 17 | 18 |
19 |

Are you building your own website? Indie reader? Personal publishing web app? Or some other digital magic-cloud proxy? If so, come on by and join a gathering of people with likeminded interests. Bring your friends that want to start a personal web site. Exchange information, swap ideas, talk shop, help work on a project...

20 |

See the Homebrew Website Club Newsletter Volume 1 Issue 1 for a description of the first meeting.

21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/h-feed-with-child-author: -------------------------------------------------------------------------------- 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 | 32 | 33 | Author Name 34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | php-versions: ["7.2", "7.3", "7.4", "8.0"] 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Setup PHP, with composer and extensions 20 | uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php 21 | with: 22 | php-version: ${{ matrix.php-versions }} 23 | extensions: mbstring, simplexml, dom, curl 24 | coverage: pcov 25 | 26 | - name: Validate composer.json and composer.lock 27 | run: composer validate --strict 28 | 29 | - name: Cache Composer packages 30 | id: composer-cache 31 | uses: actions/cache@v2 32 | with: 33 | path: vendor 34 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 35 | restore-keys: | 36 | ${{ runner.os }}-php- 37 | 38 | - name: Install dependencies 39 | run: composer update --prefer-dist --no-progress 40 | 41 | - name: Run test suite 42 | run: composer run-script test 43 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-event-featured: -------------------------------------------------------------------------------- 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 | Homebrew Website Club 10 | 11 | 12 |

Homebrew Website Club

13 | 14 | permalink 15 | 16 | 17 | 18 | - 19 | 20 |
21 |

Are you building your own website? Indie reader? Personal publishing web app? Or some other digital magic-cloud proxy? If so, come on by and join a gathering of people with likeminded interests. Bring your friends that want to start a personal web site. Exchange information, swap ideas, talk shop, help work on a project...

22 |

See the Homebrew Website Club Newsletter Volume 1 Issue 1 for a description of the first meeting.

23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/data/source.example.com/h-event-text-content: -------------------------------------------------------------------------------- 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 | Homebrew Website Club 10 | 11 | 12 |

Homebrew Website Club

13 | 14 | permalink 15 | 16 | - 17 | 18 |
19 |

Are you building your own website? Indie reader? Personal publishing web app? Or some other digital magic-cloud proxy? If so, come on by and join a gathering of people with likeminded interests. Bring your friends that want to start a personal web site. Exchange information, swap ideas, talk shop, help work on a project...

20 |

See the Homebrew Website Club Newsletter Volume 1 Issue 1 for a description of the first meeting.

21 |
22 | 23 | Event Author 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/h-card-with-child-h-feed: -------------------------------------------------------------------------------- 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 | Author Name 15 | 16 | 34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/data/hacker-news.firebaseio.com/v0_item_14516538.json: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx 3 | Date: Thu, 08 Jun 2017 21:28:24 GMT 4 | Content-Type: application/json; charset=utf-8 5 | Content-Length: 949 6 | Connection: keep-alive 7 | Access-Control-Allow-Origin: * 8 | Cache-Control: no-cache 9 | Strict-Transport-Security: max-age=31556926; includeSubDomains; preload 10 | 11 | {"by":"vkb","descendants":51,"id":14516538,"kids":[14516923,14517320,14517322,14517224,14516999,14516850,14517290,14516926,14516808,14517088,14517137,14516981,14516706,14517080,14517055,14516805,14516785,14516890,14517104,14516723,14516853,14517094],"score":84,"text":"There have been many, many posts about how toxic advertising and Facebook are (I've written many myself[1][2][3]) for our internet ecosystem today.

What projects or companies are you working on to combat filter bubbles, walled gardens, emotional manipulation, and the like, and how can the HN community help you in your goals?

[1]http://veekaybee.github.io/facebook-is-collecting-this/\n[2]http://veekaybee.github.io/content-is-dead/\n[3] http://veekaybee.github.io/who-is-doing-this-to-my-internet/","time":1496950332,"title":"What are we doing about Facebook, Google, and the closed internet?","type":"story"} -------------------------------------------------------------------------------- /tests/data/sanitize.example/entry-with-valid-tags: -------------------------------------------------------------------------------- 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 |

This content has only valid tags. links, abbreviations, bold, inline code, delete, emphasis, italics, images are allowed inline quote, strikethrough, strong text, and are supported inline elements.

14 | 15 |
Blockquote tags are okay
16 | 17 |
preformatted text is okay too
18 | for code examples and such
19 | 20 |

Paragraph tags are allowed

21 | 22 |

We should allow
break
tags too

23 | 24 |

One

25 |

Two

26 |

Three

27 |

Four

28 |
Five
29 |
Six
30 | 31 | 36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 2 | In a future version, replies, likes, reposts, etc. of this post will be included if they are listed on the page. 3 | 4 | ```json 5 | { 6 | "data": { 7 | "type": "entry", 8 | ... 9 | "like": [ 10 | { 11 | "type": "cite", 12 | "author": { 13 | "type": "card", 14 | "name": "Thomas Dunlap", 15 | "photo": "https://s3-us-west-2.amazonaws.com/aaronparecki.com/twitter.com/9055c458a67762637c0071006b16c78f25cb610b224dbc98f48961d772faff4d.jpeg", 16 | "url": "https://twitter.com/spladow" 17 | }, 18 | "url": "https://twitter.com/aaronpk/status/688518372170977280#favorited-by-16467582" 19 | } 20 | ], 21 | "comment": [ 22 | { 23 | "type": "cite", 24 | "author": { 25 | "type": "card", 26 | "name": "Poetica", 27 | "photo": "https://s3-us-west-2.amazonaws.com/aaronparecki.com/twitter.com/192664bb706b2998ed42a50a860490b6aa1bb4926b458ba293b4578af599aa6f.png", 28 | "url": "http://poetica.com/" 29 | }, 30 | "url": "https://twitter.com/poetica/status/689045331426803712", 31 | "published": "2016-01-18T03:23:03-08:00", 32 | "content": { 33 | "text": "@aaronpk @mozillapersona thanks very much! :)" 34 | } 35 | } 36 | ] 37 | } 38 | } 39 | 40 | ``` 41 | -------------------------------------------------------------------------------- /tests/data/feed.example.com/jsonfeed-top-level-author: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Date: Sat, 11 Nov 2017 15:35:23 GMT 3 | Content-Type: application/json; charset=UTF-8 4 | 5 | { 6 | "version": "https://jsonfeed.org/version/1", 7 | "user_comment": "This feed allows you to read the posts from this site in any feed reader that supports the JSON Feed format. To add this feed to your reader, copy the following URL -- http://www.manton.org/feed/json -- and add it your reader.", 8 | "home_page_url": "http://www.manton.org", 9 | "feed_url": "http://www.manton.org/feed/json", 10 | "title": "Manton Reece", 11 | "author": { 12 | "name": "Author Name", 13 | "url": "https://author.example.com" 14 | }, 15 | "description": "", 16 | "items": [ 17 | { 18 | "id": "http://www.manton.org/2017/11/5993.html", 19 | "url": "http://www.manton.org/2017/11/5993.html", 20 | "title": "", 21 | "content_html": "

I’ve updated Micro.blog’s Twitter cross-posting to support 280 characters. The apps still color the character counter blue until 140, and red after 280, just in case you want to stick to shorter posts.

\n", 22 | "date_published": "2017-11-10T16:34:21+00:00", 23 | "date_modified": "2017-11-10T16:34:21+00:00" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /lib/XRay/Formats/HTMLPurifier_AttrDef_HTML_Microformats2.php: -------------------------------------------------------------------------------- 1 | getDefinition('HTML')->doctype->name; 19 | if ($name == "XHTML 1.1" || $name == "XHTML 2.0") { 20 | return parent::split($string, $config, $context); 21 | } else { 22 | return preg_split('/\s+/', $string); 23 | } 24 | } 25 | 26 | /** 27 | * @param array $tokens 28 | * @param HTMLPurifier_Config $config 29 | * @param HTMLPurifier_Context $context 30 | * @return array 31 | */ 32 | protected function filter($tokens, $config, $context) 33 | { 34 | $ret = array(); 35 | foreach ($tokens as $token) { 36 | if(preg_match('/^([hpue]|dt)-[a-z\-]+$/', $token)) { 37 | $ret[] = $token; 38 | } 39 | } 40 | return $ret; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/data/activitystreams.example/aaronpk: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 30 Jul 2018 03:29:14 GMT 4 | Content-Type: application/activity+json 5 | Connection: keep-alive 6 | 7 | { 8 | "@context": [ 9 | "https://www.w3.org/ns/activitystreams", 10 | "https://w3id.org/security/v1" 11 | ], 12 | "id": "https://aaronparecki.com/aaronpk", 13 | "type": "Person", 14 | "preferredUsername": "aaronpk", 15 | "url": "https://aaronparecki.com/", 16 | "icon": { 17 | "type": "Image", 18 | "mediaType": "image/jpeg", 19 | "url": "https://aaronparecki.com/images/profile.jpg" 20 | }, 21 | "image": { 22 | "type": "Image", 23 | "mediaType": "image/jpeg", 24 | "url": "https://aaronparecki.com/images/cover-photo.jpg" 25 | }, 26 | "inbox": "https://aaronparecki.com/activitypub/inbox", 27 | "outbox": "https://aaronparecki.com/activitypub/outbox", 28 | "publicKey": { 29 | "id": "https://aaronparecki.com/aaronpk#key", 30 | "owner": "https://aaronparecki.com/aaronpk", 31 | "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzlRsHgaSwaoB/AOLxjsU\nBDGDZJileQyg2ZmEhWBMLaqNMJH0EqnraQRHfnHz7Yp32zjYCj2NObaX7ySd3uUJ\nzoEyhmfx122WQupOPclGYBC7mbXXhiheDb3ItPn9TxCOveC8d5d2Bm6vyCq4m31x\nBH0jOImL3AscLNwhEdYIHvweXuIqaat50O6yrgJUadJBvw0hyPVFvwiak1dKA2Su\nHCxsgLpxasEoByJMNy1COG8AvR+SuSvwJXZ2DeDS98Ji9EbeaKl6F2mJGJC/Fe2q\nz0t3mVll8Zs4bdnraw6pcnmFNzOv0SziunJoKjQ5IgZDmWHQY4EgEybxa9SEOZ/s\ntQIDAQAB\n-----END PUBLIC KEY-----\n" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/data/source.example.com/bridgy-invitee: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 02 Dec 2015 03:30:14 GMT 4 | Content-Type: text/html; charset=utf-8 5 | Connection: keep-alive 6 | 7 | 8 | 9 | 10 | 11 | Bridgy Response 12 | 28 | 29 |
30 | tag:facebook.com,2013:555707837940351_rsvp_10153997868474283 31 | 32 | invited 33 | 34 |
35 | 36 | Tantek Çelik 37 | 38 | 39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 | -------------------------------------------------------------------------------- /controllers/Feeds.php: -------------------------------------------------------------------------------- 1 | http = new p3k\HTTP(); 12 | } 13 | 14 | private function respond(Response $response, $code, $params, $headers=[]) { 15 | $response->setStatusCode($code); 16 | foreach($headers as $k=>$v) { 17 | $response->headers->set($k, $v); 18 | } 19 | $response->headers->set('Content-Type', 'application/json'); 20 | $opts = JSON_UNESCAPED_SLASHES; 21 | if($this->_pretty) $opts += JSON_PRETTY_PRINT; 22 | $response->setContent(json_encode($params, $opts)."\n"); 23 | return $response; 24 | } 25 | 26 | public function find(Request $request, Response $response) { 27 | $opts = []; 28 | 29 | if($request->get('timeout')) { 30 | $opts['timeout'] = $request->get('timeout'); 31 | } 32 | 33 | if($request->get('max_redirects')) { 34 | $opts['max_redirects'] = (int)$request->get('max_redirects'); 35 | } 36 | 37 | if($request->get('pretty')) { 38 | $this->_pretty = true; 39 | } 40 | 41 | $url = $request->get('url'); 42 | 43 | if(!$url) { 44 | return $this->respond($response, 400, [ 45 | 'error' => 'missing_url', 46 | 'error_description' => 'Provide a URL to fetch' 47 | ]); 48 | } 49 | 50 | $xray = new p3k\XRay(); 51 | $xray->http = $this->http; 52 | $res = $xray->feeds($url, $opts); 53 | 54 | return $this->respond($response, !empty($res['error']) ? 400 : 200, $res); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /lib/XRay/HTTPSig.php: -------------------------------------------------------------------------------- 1 | 'get '.parse_url($target, PHP_URL_PATH), 25 | 'Date' => $date, 26 | 'Host' => parse_url($target, PHP_URL_HOST), 27 | 'Content-Type' => 'application/activity+json', 28 | ]; 29 | 30 | return $headers; 31 | } 32 | 33 | protected function _httpSign(&$headers, $key) { 34 | $stringToSign = $this->_headersToSigningString($headers); 35 | 36 | $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers))); 37 | 38 | $privateKey = openssl_pkey_get_private('file://'.$key['key']); 39 | 40 | openssl_sign($stringToSign, $signature, $privateKey, OPENSSL_ALGO_SHA256); 41 | $signature = base64_encode($signature); 42 | 43 | $signatureHeader = 'keyId="'.$key['keyId'].'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"'; 44 | 45 | unset($headers['(request-target)']); 46 | 47 | $headers['Signature'] = $signatureHeader; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/h-entry-with-javascript-urls: -------------------------------------------------------------------------------- 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 | 6 | 7 | 8 | 9 | 10 | Example 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 | Author 20 | 21 |
22 | 23 |

Hello World

24 | 25 | attack 26 | attack 27 | attack 28 | attack 29 | attack 30 | attack 31 | attack 32 | attack 33 | attack 34 |
35 | attack 36 |
37 |
38 | attack 39 |
40 | 41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /lib/XRay/Rels.php: -------------------------------------------------------------------------------- 1 | http = $http; 9 | } 10 | 11 | public function parse($url, $opts=[]) { 12 | if(isset($opts['timeout'])) 13 | $this->http->set_timeout($opts['timeout']); 14 | if(isset($opts['max_redirects'])) 15 | $this->http->set_max_redirects($opts['max_redirects']); 16 | 17 | $scheme = parse_url($url, PHP_URL_SCHEME); 18 | if(!in_array($scheme, ['http','https'])) { 19 | return [ 20 | 'error' => 'invalid_url', 21 | 'error_description' => 'Only http and https URLs are supported' 22 | ]; 23 | } 24 | 25 | $host = parse_url($url, PHP_URL_HOST); 26 | if(!$host) { 27 | return [ 28 | 'error' => 'invalid_url', 29 | 'error_description' => 'The URL provided was not valid' 30 | ]; 31 | } 32 | 33 | $url = normalize_url($url); 34 | 35 | $result = $this->http->get($url); 36 | 37 | $html = $result['body']; 38 | $mf2 = \mf2\Parse($html, $result['url']); 39 | 40 | $rels = $result['rels']; 41 | if(isset($mf2['rels'])) { 42 | $rels = array_merge($rels, $mf2['rels']); 43 | } 44 | 45 | // Resolve all relative URLs 46 | foreach($rels as $rel=>$values) { 47 | foreach($values as $i=>$value) { 48 | $value = \mf2\resolveUrl($result['url'], $value); 49 | $rels[$rel][$i] = $value; 50 | } 51 | } 52 | 53 | if(count($rels) == 0) 54 | $rels = new \StdClass; 55 | 56 | return [ 57 | 'url' => $result['url'], 58 | 'code' => $result['code'], 59 | 'rels' => $rels 60 | ]; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /controllers/Rels.php: -------------------------------------------------------------------------------- 1 | http = new p3k\HTTP(); 12 | } 13 | 14 | private function respond(Response $response, $code, $params, $headers=[]) { 15 | $response->setStatusCode($code); 16 | foreach($headers as $k=>$v) { 17 | $response->headers->set($k, $v); 18 | } 19 | $response->headers->set('Content-Type', 'application/json'); 20 | $opts = JSON_UNESCAPED_SLASHES; 21 | if($this->_pretty) $opts += JSON_PRETTY_PRINT; 22 | $response->setContent(json_encode($params, $opts)."\n"); 23 | return $response; 24 | } 25 | 26 | public function fetch(Request $request, Response $response) { 27 | $opts = []; 28 | 29 | if($request->get('timeout')) { 30 | // We might make 2 HTTP requests, so each request gets half the desired timeout 31 | $opts['timeout'] = $request->get('timeout') / 2; 32 | } 33 | 34 | if($request->get('max_redirects')) { 35 | $opts['max_redirects'] = (int)$request->get('max_redirects'); 36 | } 37 | 38 | if($request->get('pretty')) { 39 | $this->_pretty = true; 40 | } 41 | 42 | $url = $request->get('url'); 43 | 44 | if(!$url) { 45 | return $this->respond($response, 400, [ 46 | 'error' => 'missing_url', 47 | 'error_description' => 'Provide a URL to fetch' 48 | ]); 49 | } 50 | 51 | $xray = new p3k\XRay(); 52 | $xray->http = $this->http; 53 | $res = $xray->rels($url, $opts); 54 | 55 | return $this->respond($response, !empty($res['error']) ? 400 : 200, $res); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /tests/data/activitystreams.example/custom-emoji.json: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 30 Jul 2018 03:29:14 GMT 4 | Content-Type: application/activity+json 5 | Connection: keep-alive 6 | 7 | {"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":{"@id":"as:movedTo","@type":"@id"},"Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji","focalPoint":{"@container":"@list","@id":"toot:focalPoint"},"featured":{"@id":"toot:featured","@type":"@id"},"schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value"}],"id":"https://mastodon.social/users/Gargron/statuses/100465999501820229","type":"Note","summary":null,"inReplyTo":null,"published":"2018-07-30T22:24:54Z","url":"https://mastodon.social/@Gargron/100465999501820229","attributedTo":"https://activitystreams.example/Gargron","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://mastodon.social/users/Gargron/followers"],"sensitive":false,"atomUri":"https://mastodon.social/users/Gargron/statuses/100465999501820229","inReplyToAtomUri":null,"conversation":"tag:mastodon.social,2018-07-30:objectId=44299858:objectType=Conversation","content":"\u003cp\u003e:yikes:\u003c/p\u003e","contentMap":{"en":"\u003cp\u003e:yikes:\u003c/p\u003e"},"attachment":[],"tag":[{"id":"https://mastodon.social/emojis/31275","type":"Emoji","name":":yikes:","updated":"2018-07-15T17:28:20Z","icon":{"type":"Image","mediaType":"image/png","url":"https://files.mastodon.social/custom_emojis/images/000/031/275/original/yikes.png"}}]} 8 | -------------------------------------------------------------------------------- /tests/data/source.example.com/bridgy-example: -------------------------------------------------------------------------------- 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 | I'm at SmartMathPro by P'Pan (@t Once Academy) in Bangrak, Bangkok swarmapp.com/c/4m4AboRbm45 12 | 28 | 29 |
30 | tag:twitter.com,2013:705248246067806209 31 | 32 | 33 | 34 | 35 | ณณแนน 36 | 37 | 38 | 39 | https://twitter.com/NNanpartoonoi/status/705248246067806209 40 |
41 | 42 | I'm at SmartMathPro by P'Pan (@t Once Academy) in Bangrak, Bangkok swarmapp.com/c/4m4AboRbm45 43 |
44 | 45 | 46 | 47 | 48 | ⚡️ 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /lib/XRay/Formats/XKCD.php: -------------------------------------------------------------------------------- 1 | 'entry', 29 | 'url' => $url, 30 | 'author' => [ 31 | 'type' => 'card', 32 | 'name' => 'XKCD', 33 | 'photo' => Config::$base.'/images/xkcd.png', 34 | 'url' => 'https://xkcd.com/' 35 | ] 36 | ]; 37 | 38 | $name = $doc->getElementById('ctitle'); 39 | 40 | if(!$name) 41 | return self::_unknown(); 42 | 43 | $entry['name'] = $name->nodeValue; 44 | 45 | $photo = $xpath->query("//div[@id='comic']/img"); 46 | if($photo->length != 1) 47 | return self::_unknown(); 48 | 49 | $photo = $photo->item(0); 50 | $img1 = $photo->getAttribute('src'); 51 | $img2 = $photo->getAttribute('srcset'); 52 | if($img2) { 53 | $img2 = explode(',', $img2)[0]; 54 | if(preg_match('/([^ ]+)/', $img2, $match)) { 55 | $img2 = $match[1]; 56 | } 57 | } 58 | 59 | $src = \Mf2\resolveUrl($url, $img2 ?: $img1); 60 | 61 | $entry['photo'] = [$src]; 62 | 63 | $entry['post-type'] = \p3k\XRay\PostType::discover($entry); 64 | 65 | $response = [ 66 | 'data' => $entry, 67 | 'source-format' => 'xkcd', 68 | ]; 69 | 70 | return $response; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /tests/MediaTypeTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('text', $type->type); 9 | $this->assertEquals('html', $type->subtype); 10 | $this->assertEquals('html', $type->format); 11 | $this->assertEquals(null, $type->charset); 12 | } 13 | 14 | public function testParseTextHtmlUtf8() 15 | { 16 | $type = new p3k\XRay\MediaType('text/html; charset=UTF-8'); 17 | $this->assertEquals('text', $type->type); 18 | $this->assertEquals('html', $type->subtype); 19 | $this->assertEquals('html', $type->format); 20 | $this->assertEquals('UTF-8', $type->charset); 21 | } 22 | 23 | public function testParseTextHtmlUtf8Extra() 24 | { 25 | $type = new p3k\XRay\MediaType('text/html; hello=world; charset=UTF-8'); 26 | $this->assertEquals('text', $type->type); 27 | $this->assertEquals('html', $type->subtype); 28 | $this->assertEquals('html', $type->format); 29 | $this->assertEquals('UTF-8', $type->charset); 30 | } 31 | 32 | public function testParseApplicationJson() 33 | { 34 | $type = new p3k\XRay\MediaType('application/json'); 35 | $this->assertEquals('application', $type->type); 36 | $this->assertEquals('json', $type->subtype); 37 | $this->assertEquals('json', $type->format); 38 | $this->assertEquals(null, $type->charset); 39 | } 40 | 41 | public function testParseApplicationJsonFeed() 42 | { 43 | $type = new p3k\XRay\MediaType('application/feed+json'); 44 | $this->assertEquals('application', $type->type); 45 | $this->assertEquals('feed+json', $type->subtype); 46 | $this->assertEquals('json', $type->format); 47 | $this->assertEquals(null, $type->charset); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /lib/XRay/PostType.php: -------------------------------------------------------------------------------- 1 | https://www.w3.org/wiki/Socialwg), and have been organizing IndieWebCamp (https://indieweb.org/) conferences in this space for the last 7 years. We've been making a lot of progress:

* https://www.w3.org/TR/webmention/ - cross-site commenting

* https://www.w3.org/TR/micropub/ - API for apps to create posts on various servers

* https://www.w3.org/TR/websub/ - realtime subscriptions to feeds

* More: https://indieweb.org/specs

We focus on making sure there are a plurality of implementations and approaches rather than trying to build a single software solution to solve everything.

Try commenting on my copy of this post on my website by sending me a webmention! https://aaronparecki.com/2017/06/08/9/indieweb","time":1496953400,"type":"comment"} -------------------------------------------------------------------------------- /tests/data/source.example.com/quotation-of: -------------------------------------------------------------------------------- 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 | tag:twitter.com:1015014506651668480 9 | 10 | 11 | 12 | 13 | 14 | 15 | Michael Bishop 16 | miklb 17 | 18 | 19 | 20 | https://twitter.com/miklb/status/1015014506651668480 21 |
22 | 23 | I’m so making this into a t-shirt 24 |
25 | 26 | 27 | 28 | 29 | 30 |
31 | tag:twitter.com:1015005409726357504 32 | 33 | 34 | 35 | 36 | 37 | 38 | Developers Swearing 39 | gitlost 40 | 41 | 42 | 43 | https://twitter.com/gitlost/status/1015005409726357504 44 |
45 | 46 | Still can't git fer shit 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 | -------------------------------------------------------------------------------- /tests/HelpersTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('http://example.com/', $result); 10 | } 11 | 12 | public function testAddsSlashToBareDomain() 13 | { 14 | $url = 'http://example.com'; 15 | $result = p3k\XRay\normalize_url($url); 16 | $this->assertEquals('http://example.com/', $result); 17 | } 18 | 19 | public function testDoesNotModify() 20 | { 21 | $url = 'https://example.com/'; 22 | $result = p3k\XRay\normalize_url($url); 23 | $this->assertEquals('https://example.com/', $result); 24 | } 25 | 26 | public function testURLEquality() 27 | { 28 | $url1 = 'https://example.com/'; 29 | $url2 = 'https://example.com'; 30 | $result = p3k\XRay\urls_are_equal($url1, $url2); 31 | $this->assertEquals(true, $result); 32 | } 33 | 34 | public function testFindMicroformatsByType() 35 | { 36 | $html = << 38 |
39 | Author 40 |
41 |
42 |
43 | Author 44 |
45 |
46 |
47 | Author 48 |
49 | 50 |
51 | Author 52 |
53 | EOF; 54 | 55 | $mf2 = \Mf2\parse($html); 56 | $hcards = \p3k\XRay\Formats\Mf2::findAllMicroformatsByType($mf2, 'h-card'); 57 | $this->assertEquals('/1', $hcards[0]['properties']['url'][0]); 58 | $this->assertEquals('/2', $hcards[1]['properties']['url'][0]); 59 | $this->assertEquals('/3', $hcards[2]['properties']['url'][0]); 60 | $this->assertEquals('/4', $hcards[3]['properties']['url'][0]); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /tests/data/activitystreams.example/like.json: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 30 Jul 2018 03:29:14 GMT 4 | Content-Type: application/activity+json 5 | Connection: keep-alive 6 | 7 | { 8 | "@context": [ 9 | "https://www.w3.org/ns/activitystreams", 10 | "https://w3id.org/security/v1", 11 | { 12 | "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", 13 | "sensitive": "as:sensitive", 14 | "movedTo": { 15 | "@id": "as:movedTo", 16 | "@type": "@id" 17 | }, 18 | "Hashtag": "as:Hashtag", 19 | "ostatus": "http://ostatus.org#", 20 | "atomUri": "ostatus:atomUri", 21 | "inReplyToAtomUri": "ostatus:inReplyToAtomUri", 22 | "conversation": "ostatus:conversation", 23 | "toot": "http://joinmastodon.org/ns#", 24 | "Emoji": "toot:Emoji", 25 | "focalPoint": { 26 | "@container": "@list", 27 | "@id": "toot:focalPoint" 28 | }, 29 | "featured": { 30 | "@id": "toot:featured", 31 | "@type": "@id" 32 | }, 33 | "schema": "http://schema.org#", 34 | "PropertyValue": "schema:PropertyValue", 35 | "value": "schema:value" 36 | } 37 | ], 38 | "id": "http://activitystreams.example/Gargron#likes/9165926", 39 | "type": "Like", 40 | "actor": "http://activitystreams.example/Gargron", 41 | "object": "http://activitystreams.example/note.json", 42 | "signature": { 43 | "type": "RsaSignature2017", 44 | "creator": "http://activitystreams.example/Gargron#main-key", 45 | "created": "2018-07-31T02:35:52Z", 46 | "signatureValue": "BPNSmAPsULjeyoA67QmlYOIJiOZJcoAcczl9326O2mqd4P40yu+IL7B6J2EbWGkXoJiP/Pd+cVV/8+Anpzmo994Xc39R8STE/QRzHuLPOf0ZNB/1xf2wqPqf5H74D8sehzOexcEGIy9vfdLmSI5bpBoU1S9ljhfC/18YwaC30eKD3gsqu4qnlNw6EgOfutKYrCBwOT2+pVvbH5D8eMFMZ//RGyWJw/DMRoW7m/8yOsGOmc+lOw6ufQpDfhEKp+MET2RbAfubMbsec6puPhXGgx9Lzl9T9s9j+Ile37juhX/miN990t7mx2RmjjYSSdXw2anRd7CaSnnXnwnpaGxEiQ==" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/helpers.php: -------------------------------------------------------------------------------- 1 | render($template, $data); 7 | } 8 | 9 | // Adds slash if no path is in the URL, and convert hostname to lowercase 10 | function normalize_url($url) { 11 | $parts = parse_url($url); 12 | if(empty($parts['path'])) 13 | $parts['path'] = '/'; 14 | if(isset($parts['host'])) 15 | $parts['host'] = strtolower($parts['host']); 16 | return build_url($parts); 17 | } 18 | 19 | function normalize_urls($urls) { 20 | return array_map('\p3k\XRay\normalize_url', $urls); 21 | } 22 | 23 | function urls_are_equal($url1, $url2) { 24 | $url1 = normalize_url($url1); 25 | $url2 = normalize_url($url2); 26 | return $url1 == $url2; 27 | } 28 | 29 | function build_url($parsed_url) { 30 | $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : ''; 31 | $host = isset($parsed_url['host']) ? $parsed_url['host'] : ''; 32 | $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; 33 | $user = isset($parsed_url['user']) ? $parsed_url['user'] : ''; 34 | $pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : ''; 35 | $pass = ($user || $pass) ? "$pass@" : ''; 36 | $path = isset($parsed_url['path']) ? $parsed_url['path'] : ''; 37 | $query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : ''; 38 | $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : ''; 39 | return "$scheme$user$pass$host$port$path$query$fragment"; 40 | } 41 | 42 | function should_follow_redirects($url) { 43 | $host = parse_url($url, PHP_URL_HOST); 44 | if(preg_match('/blogspot\.com|youtube\.com/', $host)) { 45 | return false; 46 | } else { 47 | return true; 48 | } 49 | } 50 | 51 | function phpmf2_version() { 52 | $composer = json_decode(file_get_contents(dirname(__FILE__).'/../composer.lock')); 53 | $version = 'unknown'; 54 | foreach($composer->packages as $pkg) { 55 | if($pkg->name == 'mf2/mf2') { 56 | $version = $pkg->version; 57 | } 58 | } 59 | return $version; 60 | } 61 | 62 | function allow_iframe_video($value = NULL) { 63 | static $allow_iframe_video = false; 64 | 65 | if (isset($value)) 66 | $allow_iframe_video = $value; 67 | 68 | return $allow_iframe_video; 69 | } 70 | -------------------------------------------------------------------------------- /tests/data/api.github.com/users_sebsel: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: GitHub.com 3 | Date: Sat, 22 Apr 2017 20:46:05 GMT 4 | Content-Type: application/json; charset=utf-8 5 | Content-Length: 1258 6 | Status: 200 OK 7 | X-RateLimit-Limit: 60 8 | X-RateLimit-Remaining: 57 9 | X-RateLimit-Reset: 1492894908 10 | Cache-Control: public, max-age=60, s-maxage=60 11 | Vary: Accept 12 | ETag: "525266177afd22f738e334958dedfc8f" 13 | Last-Modified: Tue, 11 Apr 2017 10:12:57 GMT 14 | X-GitHub-Media-Type: github.v3; format=json 15 | Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 16 | Access-Control-Allow-Origin: * 17 | Content-Security-Policy: default-src 'none' 18 | Strict-Transport-Security: max-age=31536000; includeSubdomains; preload 19 | X-Content-Type-Options: nosniff 20 | X-Frame-Options: deny 21 | X-XSS-Protection: 1; mode=block 22 | Vary: Accept-Encoding 23 | X-Served-By: eef8b8685a106934dcbb4b7c59fba0bf 24 | X-GitHub-Request-Id: CAE2:2FD0:280DE43:3218CDC:58FBC10C 25 | 26 | { 27 | "login": "sebsel", 28 | "id": 16517999, 29 | "avatar_url": "https://avatars3.githubusercontent.com/u/16517999?v=3", 30 | "gravatar_id": "", 31 | "url": "https://api.github.com/users/sebsel", 32 | "html_url": "https://github.com/sebsel", 33 | "followers_url": "https://api.github.com/users/sebsel/followers", 34 | "following_url": "https://api.github.com/users/sebsel/following{/other_user}", 35 | "gists_url": "https://api.github.com/users/sebsel/gists{/gist_id}", 36 | "starred_url": "https://api.github.com/users/sebsel/starred{/owner}{/repo}", 37 | "subscriptions_url": "https://api.github.com/users/sebsel/subscriptions", 38 | "organizations_url": "https://api.github.com/users/sebsel/orgs", 39 | "repos_url": "https://api.github.com/users/sebsel/repos", 40 | "events_url": "https://api.github.com/users/sebsel/events{/privacy}", 41 | "received_events_url": "https://api.github.com/users/sebsel/received_events", 42 | "type": "User", 43 | "site_admin": false, 44 | "name": "Sebastiaan Andeweg", 45 | "company": null, 46 | "blog": "https://seblog.nl/", 47 | "location": "Nijmegen, The Netherlands", 48 | "email": null, 49 | "hireable": null, 50 | "bio": null, 51 | "public_repos": 16, 52 | "public_gists": 2, 53 | "followers": 5, 54 | "following": 3, 55 | "created_at": "2016-01-02T15:39:15Z", 56 | "updated_at": "2017-04-11T10:12:57Z" 57 | } -------------------------------------------------------------------------------- /tests/data/api.github.com/users_aaronpk: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: GitHub.com 3 | Date: Fri, 21 Apr 2017 15:41:52 GMT 4 | Content-Type: application/json; charset=utf-8 5 | Content-Length: 1275 6 | Status: 200 OK 7 | X-RateLimit-Limit: 60 8 | X-RateLimit-Remaining: 57 9 | X-RateLimit-Reset: 1492790890 10 | Cache-Control: public, max-age=60, s-maxage=60 11 | Vary: Accept 12 | ETag: "7c20a2777e3c14616a18c07f4b8f8b64" 13 | Last-Modified: Tue, 18 Apr 2017 22:02:28 GMT 14 | X-GitHub-Media-Type: github.v3; format=json 15 | Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 16 | Access-Control-Allow-Origin: * 17 | Content-Security-Policy: default-src 'none' 18 | Strict-Transport-Security: max-age=31536000; includeSubdomains; preload 19 | X-Content-Type-Options: nosniff 20 | X-Frame-Options: deny 21 | X-XSS-Protection: 1; mode=block 22 | Vary: Accept-Encoding 23 | X-Served-By: d0b3c2c33a23690498aa8e70a435a259 24 | X-GitHub-Request-Id: C713:2FD0:192EF2D:1F4D147:58FA2840 25 | 26 | { 27 | "login": "aaronpk", 28 | "id": 113001, 29 | "avatar_url": "https://avatars2.githubusercontent.com/u/113001?v=3", 30 | "gravatar_id": "", 31 | "url": "https://api.github.com/users/aaronpk", 32 | "html_url": "https://github.com/aaronpk", 33 | "followers_url": "https://api.github.com/users/aaronpk/followers", 34 | "following_url": "https://api.github.com/users/aaronpk/following{/other_user}", 35 | "gists_url": "https://api.github.com/users/aaronpk/gists{/gist_id}", 36 | "starred_url": "https://api.github.com/users/aaronpk/starred{/owner}{/repo}", 37 | "subscriptions_url": "https://api.github.com/users/aaronpk/subscriptions", 38 | "organizations_url": "https://api.github.com/users/aaronpk/orgs", 39 | "repos_url": "https://api.github.com/users/aaronpk/repos", 40 | "events_url": "https://api.github.com/users/aaronpk/events{/privacy}", 41 | "received_events_url": "https://api.github.com/users/aaronpk/received_events", 42 | "type": "User", 43 | "site_admin": false, 44 | "name": "Aaron Parecki", 45 | "company": null, 46 | "blog": "https://aaronparecki.com", 47 | "location": "Portland, OR", 48 | "email": "aaron@parecki.com", 49 | "hireable": null, 50 | "bio": null, 51 | "public_repos": 217, 52 | "public_gists": 143, 53 | "followers": 785, 54 | "following": 51, 55 | "created_at": "2009-08-07T18:27:56Z", 56 | "updated_at": "2017-04-18T22:02:28Z" 57 | } -------------------------------------------------------------------------------- /views/index.php: -------------------------------------------------------------------------------- 1 | layout('layout', ['title' => $title]); ?> 2 | 3 |
4 | 5 |

X-Ray

6 | 7 |
8 |
9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 |
17 |
18 |
19 |
20 | 21 |
22 |
23 | 24 |

Read Me. Please file an issue if you encounter any issues.

25 | 26 |
27 | 28 | 94 | -------------------------------------------------------------------------------- /tests/data/activitystreams.example/Gargron: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Date: Wed, 30 Jul 2018 03:29:14 GMT 4 | Content-Type: application/activity+json 5 | Connection: keep-alive 6 | 7 | {"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":{"@id":"as:movedTo","@type":"@id"},"Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji","focalPoint":{"@container":"@list","@id":"toot:focalPoint"},"featured":{"@id":"toot:featured","@type":"@id"},"schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value"}],"id":"https://mastodon.social/users/Gargron","type":"Person","following":"https://mastodon.social/users/Gargron/following","followers":"https://mastodon.social/users/Gargron/followers","inbox":"https://mastodon.social/users/Gargron/inbox","outbox":"https://mastodon.social/users/Gargron/outbox","featured":"https://mastodon.social/users/Gargron/collections/featured","preferredUsername":"Gargron","name":"Eugen","summary":"\u003cp\u003eDeveloper of Mastodon. 25\u003c/p\u003e","url":"https://mastodon.social/@Gargron","manuallyApprovesFollowers":false,"publicKey":{"id":"https://mastodon.social/users/Gargron#main-key","owner":"https://mastodon.social/users/Gargron","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvXc4vkECU2/CeuSo1wtn\nFoim94Ne1jBMYxTZ9wm2YTdJq1oiZKif06I2fOqDzY/4q/S9uccrE9Bkajv1dnkO\nVm31QjWlhVpSKynVxEWjVBO5Ienue8gND0xvHIuXf87o61poqjEoepvsQFElA5ym\novljWGSA/jpj7ozygUZhCXtaS2W5AD5tnBQUpcO0lhItYPYTjnmzcc4y2NbJV8hz\n2s2G8qKv8fyimE23gY1XrPJg+cRF+g4PqFXujjlJ7MihD9oqtLGxbu7o1cifTn3x\nBfIdPythWu5b4cujNsB3m3awJjVmx+MHQ9SugkSIYXV0Ina77cTNS0M2PYiH1PFR\nTwIDAQAB\n-----END PUBLIC KEY-----\n"},"tag":[],"attachment":[{"type":"PropertyValue","name":"Patreon","value":"\u003ca href=\"https://www.patreon.com/mastodon\" rel=\"me nofollow noopener\" target=\"_blank\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"\"\u003epatreon.com/mastodon\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"},{"type":"PropertyValue","name":"E-mail","value":"eugen@zeonfederated.com"}],"endpoints":{"sharedInbox":"https://mastodon.social/inbox"},"icon":{"type":"Image","mediaType":"image/png","url":"https://files.mastodon.social/accounts/avatars/000/000/001/original/eb9e00274b135808.png"},"image":{"type":"Image","mediaType":"image/jpeg","url":"https://files.mastodon.social/accounts/headers/000/000/001/original/998815725e9554b0.jpg"}} 8 | -------------------------------------------------------------------------------- /tests/data/sanitize.example/cleverdevil: -------------------------------------------------------------------------------- 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 |
14 |
15 |
16 | 18 | Jonathan LaCour 20 | 21 | 22 |
23 |
 
24 |
25 |
26 |

27 | 28 | 30 | 31 |

32 |
33 |
34 |

Oh, how well they know me! 🥃 36 |

37 | 38 |
39 | 40 |
41 | Oh, how well they know me! 🥃 45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /lib/XRay/Formats/Hackernews.php: -------------------------------------------------------------------------------- 1 | get('https://hacker-news.firebaseio.com/v0/item/'.$match[1].'.json'); 26 | if($response['code'] != 200) { 27 | return [ 28 | 'error' => 'hackernews_error', 29 | 'error_description' => $response['body'], 30 | 'code' => $response['code'], 31 | ]; 32 | } 33 | 34 | return [ 35 | 'url' => $url, 36 | 'body' => $response['body'], 37 | 'code' => $response['code'], 38 | ]; 39 | } 40 | 41 | public static function parse($http_response) { 42 | $json = $http_response['body']; 43 | $url = $http_response['url']; 44 | 45 | $data = @json_decode($json, true); 46 | 47 | if(!$data) 48 | return self::_unknown(); 49 | 50 | $match = self::matches($url); 51 | 52 | $date = DateTime::createFromFormat('U', $data['time']); 53 | 54 | // Start building the h-entry 55 | $entry = array( 56 | 'type' => 'entry', 57 | 'url' => $url, 58 | 'author' => [ 59 | 'type' => 'card', 60 | 'name' => $data['by'], 61 | 'photo' => null, 62 | 'url' => 'https://news.ycombinator.com/user?id='.$data['by'] 63 | ], 64 | 'published' => $date->format('c'), 65 | ); 66 | 67 | if(isset($data['url'])) { 68 | $entry['bookmark-of'] = [$data['url']]; 69 | } 70 | 71 | if(isset($data['title'])) { 72 | $entry['name'] = $data['title']; 73 | } 74 | 75 | if(isset($data['text'])) { 76 | $htmlContent = trim(self::sanitizeHTML($data['text'])); 77 | $textContent = str_replace('

', "\n

", $htmlContent); 78 | $textContent = strip_tags($textContent); 79 | $entry['content'] = [ 80 | 'html' => $htmlContent, 81 | 'text' => $textContent 82 | ]; 83 | } 84 | 85 | if(isset($data['parent'])) { 86 | $entry['in-reply-to'] = ['https://news.ycombinator.com/item?id='.$data['parent']]; 87 | } 88 | 89 | $entry['post-type'] = \p3k\XRay\PostType::discover($entry); 90 | 91 | return [ 92 | 'data' => $entry, 93 | 'original' => $json, 94 | 'source-format' => 'hackernews', 95 | ]; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | addRoute('GET', '/', 'Main::index'); 24 | $router->addRoute('GET', '/parse', 'Parse::parse'); 25 | $router->addRoute('POST', '/parse', 'Parse::parse'); 26 | $router->addRoute('POST', '/token', 'Token::token'); 27 | 28 | $router->addRoute('GET', '/feeds', 'Feeds::find'); 29 | $router->addRoute('POST', '/feeds', 'Feeds::find'); 30 | 31 | $router->addRoute('GET', '/rels', 'Rels::fetch'); 32 | $router->addRoute('POST', '/rels', 'Rels::fetch'); 33 | 34 | $router->addRoute('GET', '/cert', 'Certbot::index'); 35 | $router->addRoute('GET', '/cert/auth', 'Certbot::start_auth'); 36 | $router->addRoute('GET', '/cert/logout', 'Certbot::logout'); 37 | $router->addRoute('GET', '/cert/redirect', 'Certbot::redirect'); 38 | $router->addRoute('POST', '/cert/save-challenge', 'Certbot::save_challenge'); 39 | $router->addRoute('GET', '/.well-known/acme-challenge/{token}', 'Certbot::challenge'); 40 | 41 | $dispatcher = $router->getDispatcher(); 42 | $request = Request::createFromGlobals(); 43 | 44 | try { 45 | $response = $dispatcher->dispatch($request->getMethod(), $request->getPathInfo()); 46 | $response->send(); 47 | } catch(League\Route\Http\Exception\NotFoundException $e) { 48 | $response = new Response; 49 | $response->setStatusCode(404); 50 | $response->setContent("Not Found\n"); 51 | $response->send(); 52 | } catch(League\Route\Http\Exception\MethodNotAllowedException $e) { 53 | $response = new Response; 54 | $response->setStatusCode(405); 55 | $response->setContent("Method not allowed\n"); 56 | $response->send(); 57 | } 58 | 59 | function shutdown() { 60 | $error = error_get_last(); 61 | if($error['type'] === E_ERROR) { 62 | header('HTTP/1.1 500 Server Error'); 63 | header('X-PHP-Error-Type: '.$error['type']); 64 | header('X-PHP-Error-Message: '.$error['message']); 65 | header('Content-Type: application/json'); 66 | echo json_encode([ 67 | 'error' => 'internal_error', 68 | 'error_code' => 500, 69 | 'error_description' => $error['message'], 70 | 'debug' => 'Please file an issue with any information you have about what caused this error: https://github.com/aaronpk/XRay/issues' 71 | ]); 72 | die(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/data/source.example.com/bookmark-missing-content: -------------------------------------------------------------------------------- 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 | A Brief History & Ethos of the Digital Garden 10 | 11 | 12 | 50 | 51 | 52 | 53 | 54 | 55 |

56 |

svenknebel.de

57 | 58 |
59 | 71 |
72 | 73 | 74 | -------------------------------------------------------------------------------- /tests/data/api.github.com/repos_aaronpk_XRay_issues_25: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: GitHub.com 3 | Date: Fri, 21 Apr 2017 15:57:52 GMT 4 | Content-Type: application/json; charset=utf-8 5 | Content-Length: 2188 6 | Status: 200 OK 7 | X-RateLimit-Limit: 60 8 | X-RateLimit-Remaining: 51 9 | X-RateLimit-Reset: 1492790890 10 | Cache-Control: public, max-age=60, s-maxage=60 11 | Vary: Accept 12 | ETag: "9bd5ea8062c126b9d42f9b86b74337e5" 13 | Last-Modified: Tue, 11 Apr 2017 10:12:57 GMT 14 | X-GitHub-Media-Type: github.v3; format=json 15 | Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 16 | Access-Control-Allow-Origin: * 17 | Content-Security-Policy: default-src 'none' 18 | Strict-Transport-Security: max-age=31536000; includeSubdomains; preload 19 | X-Content-Type-Options: nosniff 20 | X-Frame-Options: deny 21 | X-XSS-Protection: 1; mode=block 22 | Vary: Accept-Encoding 23 | X-Served-By: 15bc4ab707db6d6b474783868c7cc828 24 | X-GitHub-Request-Id: CE5A:2FD1:1A1AFDC:209084E:58FA2C00 25 | 26 | { 27 | "url": "https://api.github.com/repos/aaronpk/XRay/issues/25", 28 | "repository_url": "https://api.github.com/repos/aaronpk/XRay", 29 | "labels_url": "https://api.github.com/repos/aaronpk/XRay/issues/25/labels{/name}", 30 | "comments_url": "https://api.github.com/repos/aaronpk/XRay/issues/25/comments", 31 | "events_url": "https://api.github.com/repos/aaronpk/XRay/issues/25/events", 32 | "html_url": "https://github.com/aaronpk/XRay/issues/25", 33 | "id": 203380820, 34 | "number": 25, 35 | "title": "Post type discovery", 36 | "user": { 37 | "login": "sebsel", 38 | "id": 16517999, 39 | "avatar_url": "https://avatars3.githubusercontent.com/u/16517999?v=3", 40 | "gravatar_id": "", 41 | "url": "https://api.github.com/users/sebsel", 42 | "html_url": "https://github.com/sebsel", 43 | "followers_url": "https://api.github.com/users/sebsel/followers", 44 | "following_url": "https://api.github.com/users/sebsel/following{/other_user}", 45 | "gists_url": "https://api.github.com/users/sebsel/gists{/gist_id}", 46 | "starred_url": "https://api.github.com/users/sebsel/starred{/owner}{/repo}", 47 | "subscriptions_url": "https://api.github.com/users/sebsel/subscriptions", 48 | "organizations_url": "https://api.github.com/users/sebsel/orgs", 49 | "repos_url": "https://api.github.com/users/sebsel/repos", 50 | "events_url": "https://api.github.com/users/sebsel/events{/privacy}", 51 | "received_events_url": "https://api.github.com/users/sebsel/received_events", 52 | "type": "User", 53 | "site_admin": false 54 | }, 55 | "labels": [ 56 | 57 | ], 58 | "state": "open", 59 | "locked": false, 60 | "assignee": null, 61 | "assignees": [ 62 | 63 | ], 64 | "milestone": null, 65 | "comments": 3, 66 | "created_at": "2017-01-26T14:13:42Z", 67 | "updated_at": "2017-01-29T17:59:31Z", 68 | "closed_at": null, 69 | "body": "I don't know if this is the right place, but since I was trying to capture some of php-comments shortcomings, here is a shortcoming of XRay I found :)\r\n\r\n> sebsel a thing to note: php-comments gives a 'type' with values like 'reply' and 'like', but XRay just gives 'type=entry' with a 'like-of'. So they do different things.\r\nhttps://chat.indieweb.org/dev/2017-01-02#t1483372296121000\r\n\r\nhttps://www.w3.org/TR/post-type-discovery/", 70 | "closed_by": null 71 | } -------------------------------------------------------------------------------- /lib/XRay.php: -------------------------------------------------------------------------------- 1 | http = new HTTP('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36 p3k/XRay'); 11 | if (is_array($options)) { 12 | $this->defaultOptions = $options; 13 | } 14 | } 15 | 16 | public function rels($url, $opts=[]) { 17 | $rels = new XRay\Rels($this->http); 18 | // Merge provided options with default options, allowing provided options to override defaults. 19 | $opts = array_merge($this->defaultOptions, $opts); 20 | return $rels->parse($url, $opts); 21 | } 22 | 23 | public function feeds($url, $opts=[]) { 24 | $feeds = new XRay\Feeds($this->http); 25 | // Merge provided options with default options, allowing provided options to override defaults. 26 | $opts = array_merge($this->defaultOptions, $opts); 27 | return $feeds->find($url, $opts); 28 | } 29 | 30 | public function parse($url, $opts_or_body=false, $opts_for_body=[]) { 31 | if(!$opts_or_body || is_array($opts_or_body)) { 32 | $fetch = new XRay\Fetcher($this->http); 33 | if(is_array($opts_or_body)) { 34 | $fetch_opts = array_merge($this->defaultOptions, $opts_or_body); 35 | } else { 36 | $fetch_opts = $this->defaultOptions; 37 | } 38 | if(is_array($fetch_opts) && isset($fetch_opts['httpsig'])) { 39 | $fetch->httpsig($fetch_opts['httpsig']); 40 | } 41 | $response = $fetch->fetch($url, $fetch_opts); 42 | if(!empty($response['error'])) 43 | return $response; 44 | $body = $response['body']; 45 | $url = $response['url']; 46 | $code = $response['code']; 47 | $opts = is_array($opts_or_body) ? $opts_or_body : $opts_for_body; 48 | } else { 49 | $body = $opts_or_body; 50 | $opts = $opts_for_body; 51 | $code = null; 52 | $fetch = null; 53 | } 54 | $parser = new XRay\Parser($this->http, $fetch); 55 | 56 | // Merge provided options with default options, allowing provided options to override defaults. 57 | $opts = array_merge($this->defaultOptions, $opts); 58 | 59 | $result = $parser->parse([ 60 | 'body' => $body, 61 | 'url' => $url, 62 | 'code' => $code, 63 | ], $opts); 64 | 65 | if(!isset($opts['include_original']) || !$opts['include_original']) 66 | unset($result['original']); 67 | if(!isset($result['url'])) $result['url'] = $url; 68 | if(!isset($result['code'])) $result['code'] = $code; 69 | if(!isset($result['source-format'])) $result['source-format'] = null; 70 | return $result; 71 | } 72 | 73 | public function process($url, $mf2json, $opts=[]) { 74 | $parser = new XRay\Parser($this->http); 75 | // Merge provided options with default options, allowing provided options to override defaults. 76 | $opts = array_merge($this->defaultOptions, $opts); 77 | $result = $parser->parse([ 78 | 'body' => $mf2json, 79 | 'url' => $url, 80 | 'code' => null, 81 | ], $opts); 82 | if(!isset($opts['include_original']) || !$opts['include_original']) 83 | unset($result['original']); 84 | if(!isset($result['url'])) $result['url'] = $url; 85 | if(!isset($result['source-format'])) $result['source-format'] = null; 86 | return $result; 87 | } 88 | 89 | public function httpsig($opts) { 90 | $this->defaultOptions = array_merge($this->defaultOptions, ['httpsig' => $opts]); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /lib/XRay/Formats/Mf2Feed.php: -------------------------------------------------------------------------------- 1 | 'feed', 9 | 'items' => [], 10 | ]; 11 | 12 | // Given an mf2 data structure from a web page, assume it is a feed of entries 13 | // and return the XRay data structure for the feed. 14 | // Look for the first (BFS) h-feed if present, otherwise use the list of items. 15 | // Normalize this into a simpler mf2 structure, (h-feed -> h-* children) 16 | $feed = self::_findFirstOfType($mf2, 'h-feed'); 17 | if(!$feed) { 18 | // There was no h-feed. 19 | // Check for a top-level h-card with children 20 | if(isset($mf2['items'][0]) && in_array('h-card', $mf2['items'][0]['type'])) { 21 | $feed = $mf2['items'][0]; 22 | // If the h-card has children, use them, otherwise look for siblings 23 | if(!isset($feed['children'])) { 24 | $items = self::_findAllObjectsExcept($mf2, ['h-card']); 25 | $feed['children'] = $items; 26 | } 27 | } else { 28 | $children = self::_findAllObjectsExcept($mf2, ['h-card','h-feed']); 29 | $feed = [ 30 | 'type' => ['h-feed'], 31 | 'properties' => [], 32 | 'children' => $children 33 | ]; 34 | } 35 | } 36 | if(!isset($feed['children'])) 37 | $feed['children'] = []; 38 | 39 | // Now that the feed has been normalized so all the items are under "children", we 40 | // can transform each entry into the XRay format, including finding the author, etc 41 | foreach($feed['children'] as $item) { 42 | $parsed = false; 43 | if(in_array('h-entry', $item['type']) || in_array('h-cite', $item['type'])) { 44 | $parsed = self::parseAsHEntry($mf2, $item, $http, $url); 45 | } 46 | elseif(in_array('h-event', $item['type'])) { 47 | $parsed = self::parseAsHEvent($mf2, $item, $http, $url); 48 | } 49 | elseif(in_array('h-review', $item['type'])) { 50 | $parsed = self::parseAsHReview($mf2, $item, $http, $url); 51 | } 52 | elseif(in_array('h-recipe', $item['type'])) { 53 | $parsed = self::parseAsHRecipe($mf2, $item, $http, $url); 54 | } 55 | elseif(in_array('h-product', $item['type'])) { 56 | $parsed = self::parseAsHProduct($mf2, $item, $http, $url); 57 | } 58 | elseif(in_array('h-item', $item['type'])) { 59 | $parsed = self::parseAsHItem($mf2, $item, $http, $url); 60 | } 61 | elseif(in_array('h-card', $item['type'])) { 62 | $parsed = self::parseAsHCard($item, $http, $url); 63 | } 64 | if($parsed) { 65 | $data['items'][] = $parsed['data']; 66 | } 67 | } 68 | 69 | return [ 70 | 'data' => $data, 71 | 'source-format' => 'mf2+html', 72 | ]; 73 | } 74 | 75 | private static function _findFirstOfType($mf2, $type) { 76 | foreach($mf2['items'] as $item) { 77 | if(in_array($type, $item['type'])) { 78 | return $item; 79 | } else { 80 | if(isset($item['children'])) { 81 | $items = $item['children']; 82 | return self::_findFirstOfType(['items'=>$items], $type); 83 | } 84 | } 85 | } 86 | } 87 | 88 | private static function _findAllObjectsExcept($mf2, $types) { 89 | $items = []; 90 | foreach($mf2['items'] as $item) { 91 | if(count(array_intersect($item['type'], $types)) == 0) { 92 | $items[] = $item; 93 | } 94 | } 95 | return $items; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /tests/data/activitystreams.example/jamey: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Date: Tue, 31 Jul 2018 22:46:57 GMT 3 | Content-Type: application/activity+json; charset=utf-8 4 | Transfer-Encoding: chunked 5 | Connection: keep-alive 6 | Server: Mastodon 7 | X-Frame-Options: DENY 8 | X-Content-Type-Options: nosniff 9 | X-XSS-Protection: 1; mode=block 10 | Link: ; rel="lrdd"; type="application/xrd+xml", ; rel="alternate"; type="application/atom+xml", ; rel="alternate"; type="application/activity+json" 11 | Vary: Accept,Accept-Encoding 12 | Cache-Control: max-age=180, public 13 | ETag: W/"e354d3ca6ebb3ebfdfbe129a17d2cfbc" 14 | X-Request-Id: c3e7bab2-2cf2-4f2b-a380-7c6feb991704 15 | X-Runtime: 0.178355 16 | Strict-Transport-Security: max-age=31536000; includeSubDomains 17 | 18 | { 19 | "@context": [ 20 | "https://www.w3.org/ns/activitystreams", 21 | "https://w3id.org/security/v1", 22 | { 23 | "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", 24 | "sensitive": "as:sensitive", 25 | "movedTo": "as:movedTo", 26 | "Hashtag": "as:Hashtag", 27 | "ostatus": "http://ostatus.org#", 28 | "atomUri": "ostatus:atomUri", 29 | "inReplyToAtomUri": "ostatus:inReplyToAtomUri", 30 | "conversation": "ostatus:conversation", 31 | "toot": "http://joinmastodon.org/ns#", 32 | "Emoji": "toot:Emoji", 33 | "focalPoint": { 34 | "@container": "@list", 35 | "@id": "toot:focalPoint" 36 | }, 37 | "featured": "toot:featured" 38 | } 39 | ], 40 | "id": "https://toot.cat/users/jamey", 41 | "type": "Person", 42 | "following": "https://toot.cat/users/jamey/following", 43 | "followers": "https://toot.cat/users/jamey/followers", 44 | "inbox": "https://toot.cat/users/jamey/inbox", 45 | "outbox": "https://toot.cat/users/jamey/outbox", 46 | "featured": "https://toot.cat/users/jamey/collections/featured", 47 | "preferredUsername": "jamey", 48 | "name": "Jamey Sharp", 49 | "summary": "

Making the computer boxes do what they're supposed to, and nothing else : https://jamey.thesharps.us : https://liberapay.com/jamey/ : 30s, he/they

", 50 | "url": "https://toot.cat/@jamey", 51 | "manuallyApprovesFollowers": false, 52 | "publicKey": { 53 | "id": "https://toot.cat/users/jamey#main-key", 54 | "owner": "https://toot.cat/users/jamey", 55 | "publicKeyPem": "-----BEGIN PUBLIC KEY-----\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm6OHTzDVR+hineYdFpUE\r\ngNJax20CjDWGl0jslR7sfcVSvc8UZ+IjooHeq8rTDA2NhVvnDevvaBpUmVqYg5bT\r\n0b6kQnyhI/RlUtLC4DpbA2pCitzBJ9S20NCVPX2YYZg6Hpbx1AY0vre2Od6N3M1p\r\nct+peO654xz4a0sRMSgqgxzyehBc1ovW4KXL927dytWu6N/b3572qI3SoTTLHTOA\r\nKrWrXMGWbKWEEV/3FqgcSUw49Kss3wh+0nko/dyxY0+SGsckxkJQvQFQRtoGO6OQ\r\nGscuox65dokqIvyBujfmSC46F69tWNOtrRGHMQ433LmWbDZW7dGVShH5bG2itMeD\r\n8wIDAQAB\r\n-----END PUBLIC KEY-----\r\n" 56 | }, 57 | "endpoints": { 58 | "sharedInbox": "https://toot.cat/inbox" 59 | }, 60 | "icon": { 61 | "type": "Image", 62 | "mediaType": "image/jpeg", 63 | "url": "https://s3-us-west-2.amazonaws.com/tootcatapril2017/accounts/avatars/000/013/259/original/c904452a8411e4f5.jpg" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /views/certbot.php: -------------------------------------------------------------------------------- 1 | layout('layout', ['title' => $title]); ?> 2 | 3 |
4 | 5 |

X-Ray Certificate Setup

6 | 7 | 8 | 9 |
10 |

The challenge was saved and is now accessible via the .well-known path.

11 |

view challenge

12 |
13 | 14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | 22 | 23 |
24 | Signed in as Sign Out. 25 |
26 | 27 |
28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 |
36 |
37 | 38 | 39 |
40 | 45 | -------------------------------------------------------------------------------- /tests/data/api.github.com/repos_idno_Known_pulls_1690: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: GitHub.com 3 | Date: Fri, 21 Apr 2017 15:08:10 GMT 4 | Content-Type: application/json; charset=utf-8 5 | Content-Length: 2415 6 | Status: 200 OK 7 | X-RateLimit-Limit: 60 8 | X-RateLimit-Remaining: 59 9 | X-RateLimit-Reset: 1492790890 10 | Cache-Control: public, max-age=60, s-maxage=60 11 | Vary: Accept 12 | ETag: "8bb1e470c72305c381e9c1cf336f960f" 13 | Last-Modified: Thu, 20 Apr 2017 12:55:34 GMT 14 | X-GitHub-Media-Type: github.v3; format=json 15 | Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 16 | Access-Control-Allow-Origin: * 17 | Content-Security-Policy: default-src 'none' 18 | Strict-Transport-Security: max-age=31536000; includeSubdomains; preload 19 | X-Content-Type-Options: nosniff 20 | X-Frame-Options: deny 21 | X-XSS-Protection: 1; mode=block 22 | Vary: Accept-Encoding 23 | X-Served-By: e14705a23c085afeff5e104b1fc3922a 24 | X-GitHub-Request-Id: F706:2FD1:1984FFC:1FD658F:58FA2059 25 | 26 | { 27 | "url": "https://api.github.com/repos/idno/Known/issues/1690", 28 | "repository_url": "https://api.github.com/repos/idno/Known", 29 | "labels_url": "https://api.github.com/repos/idno/Known/issues/1690/labels{/name}", 30 | "comments_url": "https://api.github.com/repos/idno/Known/issues/1690/comments", 31 | "events_url": "https://api.github.com/repos/idno/Known/issues/1690/events", 32 | "html_url": "https://github.com/idno/Known/pull/1690", 33 | "id": 220719149, 34 | "number": 1690, 35 | "title": "fixes bookmark Microformats markup", 36 | "user": { 37 | "login": "aaronpk", 38 | "id": 113001, 39 | "avatar_url": "https://avatars2.githubusercontent.com/u/113001?v=3", 40 | "gravatar_id": "", 41 | "url": "https://api.github.com/users/aaronpk", 42 | "html_url": "https://github.com/aaronpk", 43 | "followers_url": "https://api.github.com/users/aaronpk/followers", 44 | "following_url": "https://api.github.com/users/aaronpk/following{/other_user}", 45 | "gists_url": "https://api.github.com/users/aaronpk/gists{/gist_id}", 46 | "starred_url": "https://api.github.com/users/aaronpk/starred{/owner}{/repo}", 47 | "subscriptions_url": "https://api.github.com/users/aaronpk/subscriptions", 48 | "organizations_url": "https://api.github.com/users/aaronpk/orgs", 49 | "repos_url": "https://api.github.com/users/aaronpk/repos", 50 | "events_url": "https://api.github.com/users/aaronpk/events{/privacy}", 51 | "received_events_url": "https://api.github.com/users/aaronpk/received_events", 52 | "type": "User", 53 | "site_admin": false 54 | }, 55 | "labels": [ 56 | 57 | ], 58 | "state": "open", 59 | "locked": false, 60 | "assignee": null, 61 | "assignees": [ 62 | 63 | ], 64 | "milestone": null, 65 | "comments": 0, 66 | "created_at": "2017-04-10T17:44:57Z", 67 | "updated_at": "2017-04-20T12:55:34Z", 68 | "closed_at": null, 69 | "pull_request": { 70 | "url": "https://api.github.com/repos/idno/Known/pulls/1690", 71 | "html_url": "https://github.com/idno/Known/pull/1690", 72 | "diff_url": "https://github.com/idno/Known/pull/1690.diff", 73 | "patch_url": "https://github.com/idno/Known/pull/1690.patch" 74 | }, 75 | "body": "## Here's what I fixed or added:\r\n\r\nI updated the bookmark template to use `u-bookmark-of` instead of `p-bookmark`. This fixes the parsed version of bookmark posts.\r\n\r\n## Here's why I did it:\r\n\r\nThe bookmark Microformats were setting only the plaintext `bookmark` property which caused bookmarks to not actually reference the URL they are bookmarks of.\r\n", 76 | "closed_by": null 77 | } -------------------------------------------------------------------------------- /lib/XRay/Formats/XML.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'type' => 'unknown', 22 | ], 23 | 'url' => $url, 24 | 'source-format' => 'xml', 25 | 'code' => $http_response['code'], 26 | ]; 27 | 28 | try { 29 | $reader = new Reader(); 30 | $parser = $reader->getParser($url, $xml, ''); 31 | $feed = $parser->execute(); 32 | 33 | $result['data']['type'] = 'feed'; 34 | $result['data']['items'] = []; 35 | 36 | foreach($feed->getItems() as $item) { 37 | $result['data']['items'][] = self::_hEntryFromFeedItem($item, $feed); 38 | } 39 | 40 | } catch(PicoFeedException $e) { 41 | 42 | } 43 | 44 | return $result; 45 | } 46 | 47 | private static function _hEntryFromFeedItem($item, $feed) { 48 | $entry = [ 49 | 'type' => 'entry', 50 | 'author' => [ 51 | 'name' => null, 52 | 'url' => null, 53 | 'photo' => null 54 | ] 55 | ]; 56 | 57 | if(is_array($guid=$item->getTag('guid')) && count($guid)) 58 | $entry['uid'] = $guid[0]; 59 | elseif(is_array($guid=$item->getTag('id')) && count($guid)) 60 | $entry['uid'] = $guid[0]; 61 | 62 | if($item->getUrl()) 63 | $entry['url'] = $item->getUrl(); 64 | 65 | if($item->getPublishedDate()) 66 | $entry['published'] = $item->getPublishedDate()->format('c'); 67 | 68 | if($item->getContent()) 69 | $entry['content'] = [ 70 | 'html' => self::sanitizeHTML($item->getContent()), 71 | 'text' => self::stripHTML($item->getContent()) 72 | ]; 73 | 74 | if($item->getTitle() && $item->getTitle() != $item->getUrl()) { 75 | $title = $item->getTitle(); 76 | $entry['name'] = $title; 77 | 78 | // Check if the title is a prefix of the content and drop if so 79 | if(isset($entry['content'])) { 80 | if(substr($title, -3) == '...' || substr($title, -1) == '…') { 81 | if(substr($title, -3) == '...') { 82 | $trimmedTitle = substr($title, 0, -3); 83 | } else { 84 | $trimmedTitle = substr($title, 0, -1); 85 | } 86 | if(substr($entry['content']['text'], 0, strlen($trimmedTitle)) == $trimmedTitle) { 87 | unset($entry['name']); 88 | } 89 | } 90 | } 91 | } 92 | 93 | if($item->getAuthor()) { 94 | $entry['author']['name'] = $item->getAuthor(); 95 | } 96 | 97 | if($item->getAuthorUrl()) { 98 | $entry['author']['url'] = $item->getAuthorUrl(); 99 | } else if($feed->siteUrl) { 100 | $entry['author']['url'] = $feed->siteUrl; 101 | } 102 | 103 | if($item->getEnclosureType()) { 104 | $prop = false; 105 | switch($item->getEnclosureType()) { 106 | case 'audio/mpeg': 107 | $prop = 'audio'; break; 108 | case 'image/jpeg': 109 | case 'image/png': 110 | case 'image/gif': 111 | $prop = 'photo'; break; 112 | } 113 | if($prop) 114 | $entry[$prop] = [$item->getEnclosureUrl()]; 115 | } 116 | 117 | $entry['post-type'] = \p3k\XRay\PostType::discover($entry); 118 | 119 | return $entry; 120 | } 121 | 122 | } 123 | --------------------------------------------------------------------------------