From 8a7f3aae42d9ef13183b43b5df30ff05ae13ad84 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Fri, 28 Apr 2017 08:17:57 -0700 Subject: [PATCH 01/17] update docs --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 532940a..d1a9c52 100644 --- a/README.md +++ b/README.md @@ -119,8 +119,14 @@ The primary object on the page is returned in the `data` property. This will ind If a property supports multiple values, it will always be returned as an array. The following properties support multiple values: * in-reply-to +* like-of +* repost-of +* bookmark-of * syndication * photo (of entry, not of a card) +* video +* audio +* category The content will be an object that always contains a "text" property and may contain an "html" property if the source documented published HTML content. The "text" property must always be HTML escaped before displaying it as HTML, as it may include unescaped characters such as `<` and `>`. From eb0184a6cb5c2ddf12603b5f8823a2ca10a4eb35 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Fri, 28 Apr 2017 08:19:17 -0700 Subject: [PATCH 02/17] add xkcd image --- public/images/xkcd.png | Bin 0 -> 37561 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/images/xkcd.png diff --git a/public/images/xkcd.png b/public/images/xkcd.png new file mode 100644 index 0000000000000000000000000000000000000000..27968b2d0a388c01106e5eda32dc87a5d4fa4fc5 GIT binary patch literal 37561 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 Date: Fri, 28 Apr 2017 08:19:25 -0700 Subject: [PATCH 03/17] update gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0f55d45..54c0511 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store config.php vendor/ -XRay-*.json \ No newline at end of file +php_errors.log +XRay-*.json From 4b76d7e853b6dc10ef03de6dde9165c4fa6b85c5 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Fri, 28 Apr 2017 09:02:56 -0700 Subject: [PATCH 04/17] update composer for split into library --- composer.json | 20 +- composer.lock | 1554 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 1422 insertions(+), 152 deletions(-) diff --git a/composer.json b/composer.json index 10d45b1..ecbf69c 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,6 @@ { + "name": "p3k/xray", "require": { - "league/plates": "3.*", - "league/route": "1.*", "mf2/mf2": "~0.3", "ezyang/htmlpurifier": "4.*", "indieweb/link-rel-parser": "0.1.*", @@ -9,14 +8,14 @@ "p3k/timezone": "*", "cebe/markdown": "~1.1.1" }, + "require-dev": { + "league/plates": "3.*", + "league/route": "1.*", + "phpunit/phpunit": "5.7.*" + }, "autoload": { "files": [ "lib/helpers.php", - "controllers/Main.php", - "controllers/Parse.php", - "controllers/Token.php", - "controllers/Rels.php", - "controllers/Certbot.php", "lib/HTTPCurl.php", "lib/HTTPStream.php", "lib/HTTP.php", @@ -30,7 +29,12 @@ }, "autoload-dev": { "files": [ - "lib/HTTPTest.php" + "lib/HTTPTest.php", + "controllers/Main.php", + "controllers/Parse.php", + "controllers/Token.php", + "controllers/Rels.php", + "controllers/Certbot.php" ] } } diff --git a/composer.lock b/composer.lock index c8bb14c..518ea95 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "cb96c21fe4605055f01f595720b750df", + "content-hash": "64404924cbc0d9b0e604d99859f3d673", "packages": [ { "name": "cebe/markdown", @@ -110,21 +110,24 @@ }, { "name": "ezyang/htmlpurifier", - "version": "v4.8.0", + "version": "v4.9.2", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "d0c392f77d2f2a3dcf7fcb79e2a1e2b8804e75b2" + "reference": "6d50e5282afdfdfc3e0ff6d192aff56c5629b3d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/d0c392f77d2f2a3dcf7fcb79e2a1e2b8804e75b2", - "reference": "d0c392f77d2f2a3dcf7fcb79e2a1e2b8804e75b2", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/6d50e5282afdfdfc3e0ff6d192aff56c5629b3d4", + "reference": "6d50e5282afdfdfc3e0ff6d192aff56c5629b3d4", "shasum": "" }, "require": { "php": ">=5.2" }, + "require-dev": { + "simpletest/simpletest": "^1.1" + }, "type": "library", "autoload": { "psr-0": { @@ -150,7 +153,7 @@ "keywords": [ "html" ], - "time": "2016-07-16T12:58:58+00:00" + "time": "2017-03-13T06:30:53+00:00" }, { "name": "indieweb/link-rel-parser", @@ -198,6 +201,150 @@ ], "time": "2017-01-11T17:14:49+00:00" }, + { + "name": "mf2/mf2", + "version": "v0.3.0", + "source": { + "type": "git", + "url": "https://github.com/indieweb/php-mf2.git", + "reference": "4fb2eb5365cbc0fd2e0c26ca748777d6c2539763" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/indieweb/php-mf2/zipball/4fb2eb5365cbc0fd2e0c26ca748777d6c2539763", + "reference": "4fb2eb5365cbc0fd2e0c26ca748777d6c2539763", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, + "suggest": { + "barnabywalters/mf-cleaner": "To more easily handle the canonical data php-mf2 gives you" + }, + "bin": [ + "bin/fetch-mf2", + "bin/parse-mf2" + ], + "type": "library", + "autoload": { + "files": [ + "Mf2/Parser.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "CC0" + ], + "authors": [ + { + "name": "Barnaby Walters", + "homepage": "http://waterpigs.co.uk" + } + ], + "description": "A pure, generic microformats2 parser — makes HTML as easy to consume as a JSON API", + "keywords": [ + "html", + "microformats", + "microformats 2", + "parser", + "semantic" + ], + "time": "2016-03-14T12:13:34+00:00" + }, + { + "name": "p3k/timezone", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/aaronpk/p3k-timezone.git", + "reference": "68d3490d896f98cf0727dc937f0bb6b045050c83" + }, + "require": { + "php": ">=5.4.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/p3k/Timezone.php" + ] + }, + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Aaron Parecki", + "homepage": "https://aaronparecki.com" + } + ], + "description": "Find the timezone of a given location", + "homepage": "https://github.com/aaronpk/p3k-timezone", + "keywords": [ + "date", + "p3k", + "timezone" + ], + "time": "2017-01-12T17:30:08+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14T21:17:01+00:00" + }, { "name": "ircmaxell/password-compat", "version": "v1.0.4", @@ -412,108 +559,46 @@ "time": "2015-09-11T07:40:31+00:00" }, { - "name": "mf2/mf2", - "version": "v0.3.0", + "name": "myclabs/deep-copy", + "version": "1.6.1", "source": { "type": "git", - "url": "https://github.com/indieweb/php-mf2.git", - "reference": "4fb2eb5365cbc0fd2e0c26ca748777d6c2539763" + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/indieweb/php-mf2/zipball/4fb2eb5365cbc0fd2e0c26ca748777d6c2539763", - "reference": "4fb2eb5365cbc0fd2e0c26ca748777d6c2539763", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102", + "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102", "shasum": "" }, "require": { "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "3.7.*" - }, - "suggest": { - "barnabywalters/mf-cleaner": "To more easily handle the canonical data php-mf2 gives you" - }, - "bin": [ - "bin/fetch-mf2", - "bin/parse-mf2" - ], - "type": "library", - "autoload": { - "files": [ - "Mf2/Parser.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "CC0" - ], - "authors": [ - { - "name": "Barnaby Walters", - "homepage": "http://waterpigs.co.uk" - } - ], - "description": "A pure, generic microformats2 parser — makes HTML as easy to consume as a JSON API", - "keywords": [ - "html", - "microformats", - "microformats 2", - "parser", - "semantic" - ], - "time": "2016-03-14T12:13:34+00:00" - }, - { - "name": "michelf/php-markdown", - "version": "1.7.0", - "source": { - "type": "git", - "url": "https://github.com/michelf/php-markdown.git", - "reference": "1f51cc520948f66cd2af8cbc45a5ee175e774220" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/michelf/php-markdown/zipball/1f51cc520948f66cd2af8cbc45a5ee175e774220", - "reference": "1f51cc520948f66cd2af8cbc45a5ee175e774220", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" + "doctrine/collections": "1.*", + "phpunit/phpunit": "~4.1" }, "type": "library", - "extra": { - "branch-alias": { - "dev-lib": "1.4.x-dev" - } - }, "autoload": { - "psr-0": { - "Michelf": "" + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Michel Fortin", - "email": "michel.fortin@michelf.ca", - "homepage": "https://michelf.ca/", - "role": "Developer" - }, - { - "name": "John Gruber", - "homepage": "https://daringfireball.net/" - } + "MIT" ], - "description": "PHP Markdown", - "homepage": "https://michelf.ca/projects/php-markdown/", + "description": "Create deep copies (clones) of your objects", + "homepage": "https://github.com/myclabs/DeepCopy", "keywords": [ - "markdown" + "clone", + "copy", + "duplicate", + "object", + "object graph" ], - "time": "2016-10-29T18:58:20+00:00" + "time": "2017-04-12T18:52:22+00:00" }, { "name": "nikic/fast-route", @@ -559,76 +644,90 @@ "time": "2016-03-25T23:46:52+00:00" }, { - "name": "p3k/timezone", - "version": "0.1.0", + "name": "phpdocumentor/reflection-common", + "version": "1.0", "source": { "type": "git", - "url": "https://github.com/aaronpk/p3k-timezone.git", - "reference": "68d3490d896f98cf0727dc937f0bb6b045050c83" + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { - "files": [ - "src/p3k/Timezone.php" - ] + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } }, + "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache-2.0" + "MIT" ], "authors": [ { - "name": "Aaron Parecki", - "homepage": "https://aaronparecki.com" + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" } ], - "description": "Find the timezone of a given location", - "homepage": "https://github.com/aaronpk/p3k-timezone", + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", "keywords": [ - "date", - "p3k", - "timezone" + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" ], - "time": "2017-01-12T17:30:08+00:00" + "time": "2015-12-27T11:43:31+00:00" }, { - "name": "symfony/http-foundation", - "version": "v2.8.15", + "name": "phpdocumentor/reflection-docblock", + "version": "3.1.1", "source": { "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "216c111ac427f5f773c6a8bfc0c15f0a7dd74876" + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/216c111ac427f5f773c6a8bfc0c15f0a7dd74876", - "reference": "216c111ac427f5f773c6a8bfc0c15f0a7dd74876", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", "shasum": "" }, "require": { - "php": ">=5.3.9", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php54": "~1.0", - "symfony/polyfill-php55": "~1.0" + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0@dev", + "phpdocumentor/type-resolver": "^0.2.0", + "webmozart/assert": "^1.0" }, "require-dev": { - "symfony/expression-language": "~2.4|~3.0.0" + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^4.4" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - }, "autoload": { "psr-4": { - "Symfony\\Component\\HttpFoundation\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -636,37 +735,1100 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Mike van Riel", + "email": "me@mikevanriel.com" } ], - "description": "Symfony HttpFoundation Component", - "homepage": "https://symfony.com", - "time": "2016-11-27T04:20:28+00:00" + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2016-09-30T07:12:33+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.3.0", + "name": "phpdocumentor/type-resolver", + "version": "0.2.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", - "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", + "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0" }, - "suggest": { - "ext-mbstring": "For best performance" + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2016-11-25T06:54:22+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "93d39f1f7f9326d746203c7c056f300f7f126073" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073", + "reference": "93d39f1f7f9326d746203c7c056f300f7f126073", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", + "sebastian/comparator": "^1.1|^2.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5|^3.2", + "phpunit/phpunit": "^4.8 || ^5.6.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2017-03-02T20:05:34+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^5.6 || ^7.0", + "phpunit/php-file-iterator": "^1.3", + "phpunit/php-text-template": "^1.2", + "phpunit/php-token-stream": "^1.4.2 || ^2.0", + "sebastian/code-unit-reverse-lookup": "^1.0", + "sebastian/environment": "^1.3.2 || ^2.0", + "sebastian/version": "^1.0 || ^2.0" + }, + "require-dev": { + "ext-xdebug": "^2.1.4", + "phpunit/phpunit": "^5.7" + }, + "suggest": { + "ext-xdebug": "^2.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2017-04-02T07:44:40+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2016-10-03T07:40:28+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2017-02-26T11:10:40+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.4.11", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e03f8f67534427a787e21a385a67ec3ca6978ea7", + "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2017-02-27T10:12:30+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "5.7.19", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/69c4f49ff376af2692bad9cebd883d17ebaa98a1", + "reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "~1.3", + "php": "^5.6 || ^7.0", + "phpspec/prophecy": "^1.6.2", + "phpunit/php-code-coverage": "^4.0.4", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": "^1.0.6", + "phpunit/phpunit-mock-objects": "^3.2", + "sebastian/comparator": "^1.2.4", + "sebastian/diff": "~1.2", + "sebastian/environment": "^1.3.4 || ^2.0", + "sebastian/exporter": "~2.0", + "sebastian/global-state": "^1.1", + "sebastian/object-enumerator": "~2.0", + "sebastian/resource-operations": "~1.0", + "sebastian/version": "~1.0.3|~2.0", + "symfony/yaml": "~2.1|~3.0" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "3.0.2" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-xdebug": "*", + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.7.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2017-04-03T02:22:27+00:00" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", + "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.6 || ^7.0", + "phpunit/php-text-template": "^1.2", + "sebastian/exporter": "^1.2 || ^2.0" + }, + "conflict": { + "phpunit/phpunit": "<5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.4" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2016-12-08T20:27:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2017-03-04T06:30:41+00:00" + }, + { + "name": "sebastian/comparator", + "version": "1.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2 || ~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2017-01-29T09:50:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2015-12-08T07:14:41+00:00" + }, + { + "name": "sebastian/environment", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2016-11-26T07:53:53+00:00" + }, + { + "name": "sebastian/exporter", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", + "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~2.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2016-11-19T08:54:04+00:00" + }, + { + "name": "sebastian/global-state", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2015-10-12T03:26:01+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1311872ac850040a79c3c058bea3e22d0f09cbb7", + "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "sebastian/recursion-context": "~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2017-02-18T15:18:39+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a", + "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2016-11-19T07:33:16+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2015-07-28T20:34:47+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v2.8.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "0717efd2f2264dbd3d8e1bc69a0418c2fd6295d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/0717efd2f2264dbd3d8e1bc69a0418c2fd6295d2", + "reference": "0717efd2f2264dbd3d8e1bc69a0418c2fd6295d2", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php54": "~1.0", + "symfony/polyfill-php55": "~1.0" + }, + "require-dev": { + "symfony/expression-language": "~2.4|~3.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpFoundation Component", + "homepage": "https://symfony.com", + "time": "2017-04-04T15:24:26+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", + "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" }, "type": "library", "extra": { @@ -820,9 +1982,113 @@ "shim" ], "time": "2016-11-14T01:06:16+00:00" + }, + { + "name": "symfony/yaml", + "version": "v3.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "62b4cdb99d52cb1ff253c465eb1532a80cebb621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/62b4cdb99d52cb1ff253c465eb1532a80cebb621", + "reference": "62b4cdb99d52cb1ff253c465eb1532a80cebb621", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "symfony/console": "~2.8|~3.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2017-03-20T09:45:15+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2016-11-23T20:04:58+00:00" } ], - "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": [], From 35669270e34b5ee561e6f8567e438ad90a9fe47e Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Fri, 28 Apr 2017 11:55:05 -0700 Subject: [PATCH 05/17] reorganize XRay classes, use p3k-http lib * removes the HTTP classes from this project and uses p3k-http library instead * reorganizes the XRay classes into a psr-4 compatible folder * moves controller autoload into -dev in preparation for turning this into a library (#17) --- composer.json | 21 +-- composer.lock | 40 ++++- controllers/Parse.php | 19 ++- controllers/Rels.php | 2 +- controllers/Token.php | 2 +- lib/HTTP.php | 56 ------- lib/HTTPCurl.php | 127 ---------------- lib/HTTPStream.php | 138 ------------------ lib/HTTPTest.php | 92 ------------ lib/{ => XRay}/Formats/GitHub.php | 0 ...TMLPurifier_AttrDef_HTML_Microformats2.php | 0 lib/{ => XRay}/Formats/Instagram.php | 0 lib/{ => XRay}/Formats/Mf2.php | 0 lib/{ => XRay}/Formats/Twitter.php | 0 lib/{ => XRay}/Formats/XKCD.php | 0 tests/AuthorTest.php | 2 +- tests/FeedTest.php | 2 +- tests/FetchTest.php | 2 +- tests/GitHubTest.php | 2 +- tests/InstagramTest.php | 2 +- tests/ParseTest.php | 2 +- tests/SanitizeTest.php | 2 +- tests/TokenTest.php | 2 +- 23 files changed, 69 insertions(+), 444 deletions(-) delete mode 100644 lib/HTTP.php delete mode 100644 lib/HTTPCurl.php delete mode 100644 lib/HTTPStream.php delete mode 100644 lib/HTTPTest.php rename lib/{ => XRay}/Formats/GitHub.php (100%) rename lib/{ => XRay}/Formats/HTMLPurifier_AttrDef_HTML_Microformats2.php (100%) rename lib/{ => XRay}/Formats/Instagram.php (100%) rename lib/{ => XRay}/Formats/Mf2.php (100%) rename lib/{ => XRay}/Formats/Twitter.php (100%) rename lib/{ => XRay}/Formats/XKCD.php (100%) diff --git a/composer.json b/composer.json index ecbf69c..8985ca1 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,14 @@ { "name": "p3k/xray", + "type": "library", "require": { "mf2/mf2": "~0.3", "ezyang/htmlpurifier": "4.*", "indieweb/link-rel-parser": "0.1.*", - "dg/twitter-php": "^3.6", + "dg/twitter-php": "3.6.*", "p3k/timezone": "*", - "cebe/markdown": "~1.1.1" + "p3k/http": "*", + "cebe/markdown": "1.1.*" }, "require-dev": { "league/plates": "3.*", @@ -14,22 +16,15 @@ "phpunit/phpunit": "5.7.*" }, "autoload": { + "psr-4": { + "XRay\\": "lib/XRay" + }, "files": [ - "lib/helpers.php", - "lib/HTTPCurl.php", - "lib/HTTPStream.php", - "lib/HTTP.php", - "lib/Formats/Mf2.php", - "lib/Formats/Instagram.php", - "lib/Formats/GitHub.php", - "lib/Formats/Twitter.php", - "lib/Formats/XKCD.php", - "lib/Formats/HTMLPurifier_AttrDef_HTML_Microformats2.php" + "lib/helpers.php" ] }, "autoload-dev": { "files": [ - "lib/HTTPTest.php", "controllers/Main.php", "controllers/Parse.php", "controllers/Token.php", diff --git a/composer.lock b/composer.lock index 518ea95..3353338 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "64404924cbc0d9b0e604d99859f3d673", + "content-hash": "10235592166c486bf7cf2601c9811861", "packages": [ { "name": "cebe/markdown", @@ -254,6 +254,44 @@ ], "time": "2016-03-14T12:13:34+00:00" }, + { + "name": "p3k/http", + "version": "0.1.1", + "source": { + "type": "git", + "url": "https://github.com/aaronpk/p3k-http.git", + "reference": "7409b0a44f190b053d694304e716de7ce5b3568b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aaronpk/p3k-http/zipball/7409b0a44f190b053d694304e716de7ce5b3568b", + "reference": "7409b0a44f190b053d694304e716de7ce5b3568b", + "shasum": "" + }, + "require": { + "indieweb/link-rel-parser": "0.1.*", + "mf2/mf2": "0.3.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "p3k\\": "src/p3k" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Parecki", + "homepage": "https://aaronparecki.com" + } + ], + "description": "A simple wrapper API around the PHP curl functions", + "homepage": "https://github.com/aaronpk/p3k-http", + "time": "2017-04-28T18:51:28+00:00" + }, { "name": "p3k/timezone", "version": "0.1.0", diff --git a/controllers/Parse.php b/controllers/Parse.php index bc764ec..b29933f 100644 --- a/controllers/Parse.php +++ b/controllers/Parse.php @@ -12,11 +12,11 @@ class Parse { private $_pretty = false; public static function useragent() { - return 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 XRay/1.0.0 ('.\Config::$base.')'; + return 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 XRay/1.0.0 ('.\Config::$base.')'; } public function __construct() { - $this->http = new p3k\HTTP(); + $this->http = new p3k\HTTP(self::useragent()); if(Config::$cache && class_exists('Memcache')) { $this->mc = new Memcache(); $this->mc->addServer('127.0.0.1'); @@ -49,11 +49,11 @@ class Parse { if($request->get('timeout')) { // We might make 2 HTTP requests, so each request gets half the desired timeout - $this->http->timeout = $request->get('timeout') / 2; + $this->http->set_timeout($request->get('timeout') / 2); } - if($request->get('max_redirects')) { - $this->http->max_redirects = (int)$request->get('max_redirects'); + if($request->get('max_redirects') !== null) { + $this->http->set_max_redirects((int)$request->get('max_redirects')); } if($request->get('pretty')) { @@ -103,6 +103,11 @@ class Parse { return $this->parseGitHubURL($request, $response, $url); } + if(!should_follow_redirects($url)) + $this->http->set_transport(new p3k\HTTP\Stream()); + else + $this->http->set_transport(new p3k\HTTP\Curl()); + // Now fetch the URL and check for any curl errors // Don't cache the response if a token is used to fetch it if($this->mc && !$request->get('token')) { @@ -111,14 +116,14 @@ class Parse { $result = json_decode($cached, true); self::debug('using HTML from cache', 'X-Cache-Debug'); } else { - $result = $this->http->get($url, [self::useragent()]); + $result = $this->http->get($url); $cacheData = json_encode($result); // App Engine limits the size of cached items, so don't cache ones larger than that if(strlen($cacheData) < 1000000) $this->mc->set($cacheKey, $cacheData, MEMCACHE_COMPRESSED, $this->_cacheTime); } } else { - $headers = [self::useragent()]; + $headers = []; if($request->get('token')) { $headers[] = 'Authorization: Bearer ' . $request->get('token'); } diff --git a/controllers/Rels.php b/controllers/Rels.php index 2bd5cf1..8cdcef5 100644 --- a/controllers/Rels.php +++ b/controllers/Rels.php @@ -70,7 +70,7 @@ class Rels { $html = $result['body']; $mf2 = mf2\Parse($html, $result['url']); - $rels = p3k\HTTP::link_rels($result['headers']); + $rels = $result['rels']; if(isset($mf2['rels'])) { $rels = array_merge($rels, $mf2['rels']); } diff --git a/controllers/Token.php b/controllers/Token.php index cc99b69..5bbcbd4 100644 --- a/controllers/Token.php +++ b/controllers/Token.php @@ -55,7 +55,7 @@ class Token { if(is_string($head['headers']['Link'])) $head['headers']['Link'] = [$head['headers']['Link']]; - $rels = p3k\HTTP::link_rels($head['headers']); + $rels = $head['rels']; $endpoint = false; if(array_key_exists('token_endpoint', $rels)) { diff --git a/lib/HTTP.php b/lib/HTTP.php deleted file mode 100644 index 9a9e19b..0000000 --- a/lib/HTTP.php +++ /dev/null @@ -1,56 +0,0 @@ -_class($url); - $http = new $class($url); - $http->timeout = $this->timeout; - $http->max_redirects = $this->max_redirects; - return $http->get($url, $headers); - } - - public function post($url, $body, $headers=[]) { - $class = $this->_class($url); - $http = new $class($url); - $http->timeout = $this->timeout; - $http->max_redirects = $this->max_redirects; - return $http->post($url, $body, $headers); - } - - public function head($url) { - $class = $this->_class($url); - $http = new $class($url); - $http->timeout = $this->timeout; - $http->max_redirects = $this->max_redirects; - return $http->head($url); - } - - private function _class($url) { - if(!should_follow_redirects($url)) { - return 'p3k\HTTPStream'; - } else { - return 'p3k\HTTPCurl'; - } - } - - public static function link_rels($header_array) { - $headers = ''; - foreach($header_array as $k=>$header) { - if(is_string($header)) { - $headers .= $k . ': ' . $header . "\r\n"; - } else { - foreach($header as $h) { - $headers .= $k . ': ' . $h . "\r\n"; - } - } - } - $rels = \IndieWeb\http_rels($headers); - return $rels; - } - -} diff --git a/lib/HTTPCurl.php b/lib/HTTPCurl.php deleted file mode 100644 index d3a42f5..0000000 --- a/lib/HTTPCurl.php +++ /dev/null @@ -1,127 +0,0 @@ -_set_curlopts($ch, $url); - if($headers) - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - $response = curl_exec($ch); - $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); - return array( - 'code' => curl_getinfo($ch, CURLINFO_HTTP_CODE), - 'headers' => self::parse_headers(trim(substr($response, 0, $header_size))), - 'body' => substr($response, $header_size), - 'error' => self::error_string_from_code(curl_errno($ch)), - 'error_description' => curl_error($ch), - 'error_code' => curl_errno($ch), - 'url' => curl_getinfo($ch, CURLINFO_EFFECTIVE_URL), - ); - } - - public function post($url, $body, $headers=[]) { - $ch = curl_init($url); - $this->_set_curlopts($ch, $url); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - if($headers) - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - $response = curl_exec($ch); - $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); - return array( - 'code' => curl_getinfo($ch, CURLINFO_HTTP_CODE), - 'headers' => self::parse_headers(trim(substr($response, 0, $header_size))), - 'body' => substr($response, $header_size), - 'error' => self::error_string_from_code(curl_errno($ch)), - 'error_description' => curl_error($ch), - 'error_code' => curl_errno($ch), - 'url' => curl_getinfo($ch, CURLINFO_EFFECTIVE_URL), - ); - } - - public function head($url) { - $ch = curl_init($url); - $this->_set_curlopts($ch, $url); - curl_setopt($ch, CURLOPT_NOBODY, true); - $response = curl_exec($ch); - return array( - 'code' => curl_getinfo($ch, CURLINFO_HTTP_CODE), - 'headers' => self::parse_headers(trim($response)), - 'error' => self::error_string_from_code(curl_errno($ch)), - 'error_description' => curl_error($ch), - 'error_code' => curl_errno($ch), - 'url' => curl_getinfo($ch, CURLINFO_EFFECTIVE_URL), - ); - } - - private function _set_curlopts($ch, $url) { - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HEADER, true); - - // Special-case appspot.com URLs to not follow redirects. - // https://cloud.google.com/appengine/docs/php/urlfetch/ - if(should_follow_redirects($url)) { - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_MAXREDIRS, $this->max_redirects); - } else { - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); - } - - curl_setopt($ch, CURLOPT_TIMEOUT_MS, round($this->timeout * 1000)); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, 2000); - } - - public static function error_string_from_code($code) { - switch($code) { - case 0: - return ''; - case CURLE_COULDNT_RESOLVE_HOST: - return 'dns_error'; - case CURLE_COULDNT_CONNECT: - return 'connect_error'; - case CURLE_OPERATION_TIMEDOUT: - return 'timeout'; - case CURLE_SSL_CONNECT_ERROR: - return 'ssl_error'; - case CURLE_SSL_CERTPROBLEM: - return 'ssl_cert_error'; - case CURLE_SSL_CIPHER: - return 'ssl_unsupported_cipher'; - case CURLE_SSL_CACERT: - return 'ssl_cert_error'; - case CURLE_TOO_MANY_REDIRECTS: - return 'too_many_redirects'; - default: - return 'unknown'; - } - } - - public static function parse_headers($headers) { - $retVal = array(); - $fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $headers)); - foreach($fields as $field) { - if(preg_match('/([^:]+): (.+)/m', $field, $match)) { - $match[1] = preg_replace_callback('/(?<=^|[\x09\x20\x2D])./', function($m) { - return strtoupper($m[0]); - }, strtolower(trim($match[1]))); - // If there's already a value set for the header name being returned, turn it into an array and add the new value - $match[1] = preg_replace_callback('/(?<=^|[\x09\x20\x2D])./', function($m) { - return strtoupper($m[0]); - }, strtolower(trim($match[1]))); - if(isset($retVal[$match[1]])) { - if(!is_array($retVal[$match[1]])) - $retVal[$match[1]] = array($retVal[$match[1]]); - $retVal[$match[1]][] = $match[2]; - } else { - $retVal[$match[1]] = trim($match[2]); - } - } - } - return $retVal; - } -} diff --git a/lib/HTTPStream.php b/lib/HTTPStream.php deleted file mode 100644 index bb481d3..0000000 --- a/lib/HTTPStream.php +++ /dev/null @@ -1,138 +0,0 @@ -_stream_context('GET', $url, false, $headers); - return $this->_fetch($url, $context); - } - - public function post($url, $body, $headers=[]) { - set_error_handler("p3k\HTTPStream::exception_error_handler"); - $context = $this->_stream_context('POST', $url, $body, $headers); - return $this->_fetch($url, $context); - } - - public function head($url) { - set_error_handler("p3k\HTTPStream::exception_error_handler"); - $context = $this->_stream_context('HEAD', $url); - return $this->_fetch($url, $context); - } - - private function _fetch($url, $context) { - $error = false; - - try { - $body = file_get_contents($url, false, $context); - // This sets $http_response_header - // see http://php.net/manual/en/reserved.variables.httpresponseheader.php - } catch(\Exception $e) { - $body = false; - $http_response_header = []; - $description = str_replace('file_get_contents(): ', '', $e->getMessage()); - $code = 'unknown'; - - if(preg_match('/getaddrinfo failed/', $description)) { - $code = 'dns_error'; - $description = str_replace('php_network_getaddresses: ', '', $description); - } - - if(preg_match('/timed out|request failed/', $description)) { - $code = 'timeout'; - } - - if(preg_match('/certificate/', $description)) { - $code = 'ssl_error'; - } - - $error = [ - 'description' => $description, - 'code' => $code - ]; - } - - return array( - 'code' => self::parse_response_code($http_response_header), - 'headers' => self::parse_headers($http_response_header), - 'body' => $body, - 'error' => $error ? $error['code'] : false, - 'error_description' => $error ? $error['description'] : false, - 'url' => $url, - ); - } - - private function _stream_context($method, $url, $body=false, $headers=[]) { - $options = [ - 'method' => $method, - 'timeout' => $this->timeout, - 'ignore_errors' => true, - ]; - - if($body) { - $options['content'] = $body; - } - - if($headers) { - $options['header'] = implode("\r\n", $headers); - } - - // Special-case appspot.com URLs to not follow redirects. - // https://cloud.google.com/appengine/docs/php/urlfetch/ - if(should_follow_redirects($url)) { - $options['follow_location'] = 1; - $options['max_redirects'] = $this->max_redirects; - } else { - $options['follow_location'] = 0; - } - - return stream_context_create(['http' => $options]); - } - - public static function parse_response_code($headers) { - // When a response is a redirect, we want to find the last occurrence of the HTTP code - $code = false; - foreach($headers as $field) { - if(preg_match('/HTTP\/\d\.\d (\d+)/', $field, $match)) { - $code = $match[1]; - } - } - return $code; - } - - public static function parse_headers($headers) { - $retVal = array(); - foreach($headers as $field) { - if(preg_match('/([^:]+): (.+)/m', $field, $match)) { - $match[1] = preg_replace_callback('/(?<=^|[\x09\x20\x2D])./', function($m) { - return strtoupper($m[0]); - }, strtolower(trim($match[1]))); - // If there's already a value set for the header name being returned, turn it into an array and add the new value - $match[1] = preg_replace_callback('/(?<=^|[\x09\x20\x2D])./', function($m) { - return strtoupper($m[0]); - }, strtolower(trim($match[1]))); - if(isset($retVal[$match[1]])) { - if(!is_array($retVal[$match[1]])) - $retVal[$match[1]] = array($retVal[$match[1]]); - $retVal[$match[1]][] = $match[2]; - } else { - $retVal[$match[1]] = trim($match[2]); - } - } - } - return $retVal; - } - -} diff --git a/lib/HTTPTest.php b/lib/HTTPTest.php deleted file mode 100644 index e607086..0000000 --- a/lib/HTTPTest.php +++ /dev/null @@ -1,92 +0,0 @@ -_testDataPath = $testDataPath; - } - - public function get($url, $headers=[]) { - $this->_redirects_remaining = $this->max_redirects; - $parts = parse_url($url); - unset($parts['fragment']); - $url = \build_url($parts); - return $this->_read_file($url); - } - - public function post($url, $body, $headers=[]) { - return $this->_read_file($url); - } - - public function head($url) { - $response = $this->_read_file($url); - return array( - 'code' => $response['code'], - 'headers' => $response['headers'], - 'error' => '', - 'error_description' => '', - 'url' => $response['url'] - ); - } - - private function _read_file($url) { - $parts = parse_url($url); - if($parts['path']) { - $parts['path'] = '/'.str_replace('/','_',substr($parts['path'],1)); - $url = \build_url($parts); - } - - $filename = $this->_testDataPath.preg_replace('/https?:\/\//', '', $url); - if(!file_exists($filename)) { - $filename = $this->_testDataPath.'404.response.txt'; - } - $response = file_get_contents($filename); - - $split = explode("\r\n\r\n", $response); - if(count($split) < 2) { - throw new \Exception("Invalid file contents in test data, check that newlines are CRLF: $url"); - } - $headers = array_shift($split); - $body = implode("\r\n", $split); - - if(preg_match('/HTTP\/1\.1 (\d+)/', $headers, $match)) { - $code = $match[1]; - } - - $headers = preg_replace('/HTTP\/1\.1 \d+ .+/', '', $headers); - $parsedHeaders = self::parse_headers($headers); - - if(array_key_exists('Location', $parsedHeaders)) { - $effectiveUrl = \mf2\resolveUrl($url, $parsedHeaders['Location']); - if($this->_redirects_remaining > 0) { - $this->_redirects_remaining--; - return $this->_read_file($effectiveUrl); - } else { - return [ - 'code' => 0, - 'headers' => $parsedHeaders, - 'body' => $body, - 'error' => 'too_many_redirects', - 'error_description' => '', - 'url' => $effectiveUrl - ]; - } - } else { - $effectiveUrl = $url; - } - - return array( - 'code' => $code, - 'headers' => $parsedHeaders, - 'body' => $body, - 'error' => (isset($parsedHeaders['X-Test-Error']) ? $parsedHeaders['X-Test-Error'] : ''), - 'error_description' => '', - 'url' => $effectiveUrl - ); - } - -} diff --git a/lib/Formats/GitHub.php b/lib/XRay/Formats/GitHub.php similarity index 100% rename from lib/Formats/GitHub.php rename to lib/XRay/Formats/GitHub.php diff --git a/lib/Formats/HTMLPurifier_AttrDef_HTML_Microformats2.php b/lib/XRay/Formats/HTMLPurifier_AttrDef_HTML_Microformats2.php similarity index 100% rename from lib/Formats/HTMLPurifier_AttrDef_HTML_Microformats2.php rename to lib/XRay/Formats/HTMLPurifier_AttrDef_HTML_Microformats2.php diff --git a/lib/Formats/Instagram.php b/lib/XRay/Formats/Instagram.php similarity index 100% rename from lib/Formats/Instagram.php rename to lib/XRay/Formats/Instagram.php diff --git a/lib/Formats/Mf2.php b/lib/XRay/Formats/Mf2.php similarity index 100% rename from lib/Formats/Mf2.php rename to lib/XRay/Formats/Mf2.php diff --git a/lib/Formats/Twitter.php b/lib/XRay/Formats/Twitter.php similarity index 100% rename from lib/Formats/Twitter.php rename to lib/XRay/Formats/Twitter.php diff --git a/lib/Formats/XKCD.php b/lib/XRay/Formats/XKCD.php similarity index 100% rename from lib/Formats/XKCD.php rename to lib/XRay/Formats/XKCD.php diff --git a/tests/AuthorTest.php b/tests/AuthorTest.php index bf41b3d..975468b 100644 --- a/tests/AuthorTest.php +++ b/tests/AuthorTest.php @@ -8,7 +8,7 @@ class AuthorTest extends PHPUnit_Framework_TestCase { public function setUp() { $this->client = new Parse(); - $this->client->http = new p3k\HTTPTest(dirname(__FILE__).'/data/'); + $this->client->http = new p3k\HTTP\Test(dirname(__FILE__).'/data/'); $this->client->mc = null; } diff --git a/tests/FeedTest.php b/tests/FeedTest.php index 7acefc1..823ed94 100644 --- a/tests/FeedTest.php +++ b/tests/FeedTest.php @@ -8,7 +8,7 @@ class FeedTest extends PHPUnit_Framework_TestCase { public function setUp() { $this->client = new Parse(); - $this->client->http = new p3k\HTTPTest(dirname(__FILE__).'/data/'); + $this->client->http = new p3k\HTTP\Test(dirname(__FILE__).'/data/'); $this->client->mc = null; } diff --git a/tests/FetchTest.php b/tests/FetchTest.php index 9396b62..08509c5 100644 --- a/tests/FetchTest.php +++ b/tests/FetchTest.php @@ -8,7 +8,7 @@ class FetchTest extends PHPUnit_Framework_TestCase { public function setUp() { $this->client = new Parse(); - $this->client->http = new p3k\HTTPTest(dirname(__FILE__).'/data/'); + $this->client->http = new p3k\HTTP\Test(dirname(__FILE__).'/data/'); $this->client->mc = null; } diff --git a/tests/GitHubTest.php b/tests/GitHubTest.php index 6aef9c1..2bbad01 100644 --- a/tests/GitHubTest.php +++ b/tests/GitHubTest.php @@ -8,7 +8,7 @@ class GitHubTest extends PHPUnit_Framework_TestCase { public function setUp() { $this->client = new Parse(); - $this->client->http = new p3k\HTTPTest(dirname(__FILE__).'/data/'); + $this->client->http = new p3k\HTTP\Test(dirname(__FILE__).'/data/'); $this->client->mc = null; } diff --git a/tests/InstagramTest.php b/tests/InstagramTest.php index 4681453..2c15845 100644 --- a/tests/InstagramTest.php +++ b/tests/InstagramTest.php @@ -8,7 +8,7 @@ class InstagramTest extends PHPUnit_Framework_TestCase { public function setUp() { $this->client = new Parse(); - $this->client->http = new p3k\HTTPTest(dirname(__FILE__).'/data/'); + $this->client->http = new p3k\HTTP\Test(dirname(__FILE__).'/data/'); $this->client->mc = null; } diff --git a/tests/ParseTest.php b/tests/ParseTest.php index 579d07c..d4e45eb 100644 --- a/tests/ParseTest.php +++ b/tests/ParseTest.php @@ -8,7 +8,7 @@ class ParseTest extends PHPUnit_Framework_TestCase { public function setUp() { $this->client = new Parse(); - $this->client->http = new p3k\HTTPTest(dirname(__FILE__).'/data/'); + $this->client->http = new p3k\HTTP\Test(dirname(__FILE__).'/data/'); $this->client->mc = null; } diff --git a/tests/SanitizeTest.php b/tests/SanitizeTest.php index f7aa4a7..1865db6 100644 --- a/tests/SanitizeTest.php +++ b/tests/SanitizeTest.php @@ -8,7 +8,7 @@ class SanitizeTest extends PHPUnit_Framework_TestCase { public function setUp() { $this->client = new Parse(); - $this->client->http = new p3k\HTTPTest(dirname(__FILE__).'/data/'); + $this->client->http = new p3k\HTTP\Test(dirname(__FILE__).'/data/'); $this->client->mc = null; } diff --git a/tests/TokenTest.php b/tests/TokenTest.php index 74b7027..2988faf 100644 --- a/tests/TokenTest.php +++ b/tests/TokenTest.php @@ -8,7 +8,7 @@ class TokenTest extends PHPUnit_Framework_TestCase { public function setUp() { $this->client = new Token(); - $this->client->http = new p3k\HTTPTest(dirname(__FILE__).'/data/'); + $this->client->http = new p3k\HTTP\Test(dirname(__FILE__).'/data/'); } private function token($params) { From 5221cf79e9876c75ab303b7441f8d9de61de6215 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Fri, 28 Apr 2017 12:55:09 -0700 Subject: [PATCH 06/17] get rid of global functions moves XRay classes to `p3k\XRay` namespace --- composer.json | 2 +- composer.lock | 10 +++---- controllers/Certbot.php | 4 +-- controllers/Main.php | 2 +- controllers/Parse.php | 12 +++++--- controllers/Rels.php | 2 +- lib/XRay/Formats/GitHub.php | 2 +- ...TMLPurifier_AttrDef_HTML_Microformats2.php | 2 +- lib/XRay/Formats/Instagram.php | 2 +- lib/XRay/Formats/Mf2.php | 29 +++---------------- lib/XRay/Formats/Twitter.php | 2 +- lib/XRay/Formats/XKCD.php | 2 +- lib/helpers.php | 1 + tests/HelpersTest.php | 10 +++++-- 14 files changed, 36 insertions(+), 46 deletions(-) diff --git a/composer.json b/composer.json index 8985ca1..c0afae7 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ }, "autoload": { "psr-4": { - "XRay\\": "lib/XRay" + "p3k\\XRay\\": "lib/XRay" }, "files": [ "lib/helpers.php" diff --git a/composer.lock b/composer.lock index 3353338..ca69224 100644 --- a/composer.lock +++ b/composer.lock @@ -256,16 +256,16 @@ }, { "name": "p3k/http", - "version": "0.1.1", + "version": "0.1.4", "source": { "type": "git", "url": "https://github.com/aaronpk/p3k-http.git", - "reference": "7409b0a44f190b053d694304e716de7ce5b3568b" + "reference": "136aac6f7ecd6d6e16e8ff9286b43110680c49ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aaronpk/p3k-http/zipball/7409b0a44f190b053d694304e716de7ce5b3568b", - "reference": "7409b0a44f190b053d694304e716de7ce5b3568b", + "url": "https://api.github.com/repos/aaronpk/p3k-http/zipball/136aac6f7ecd6d6e16e8ff9286b43110680c49ab", + "reference": "136aac6f7ecd6d6e16e8ff9286b43110680c49ab", "shasum": "" }, "require": { @@ -290,7 +290,7 @@ ], "description": "A simple wrapper API around the PHP curl functions", "homepage": "https://github.com/aaronpk/p3k-http", - "time": "2017-04-28T18:51:28+00:00" + "time": "2017-04-28T19:46:12+00:00" }, { "name": "p3k/timezone", diff --git a/controllers/Certbot.php b/controllers/Certbot.php index 8fd2738..96afb1c 100644 --- a/controllers/Certbot.php +++ b/controllers/Certbot.php @@ -13,7 +13,7 @@ class Certbot { $state = mt_rand(10000,99999); $_SESSION['state'] = $state; - $response->setContent(view('certbot', [ + $response->setContent(p3k\XRay\view('certbot', [ 'title' => 'X-Ray', 'state' => $state ])); @@ -109,7 +109,7 @@ class Certbot { 'challenge' => $challenge ]), 0, 600); - $response->setContent(view('certbot', [ + $response->setContent(p3k\XRay\view('certbot', [ 'title' => 'X-Ray', 'challenge' => $challenge, 'token' => $token, diff --git a/controllers/Main.php b/controllers/Main.php index 6f78ba2..62204d5 100644 --- a/controllers/Main.php +++ b/controllers/Main.php @@ -5,7 +5,7 @@ use Symfony\Component\HttpFoundation\Response; class Main { public function index(Request $request, Response $response) { - $response->setContent(view('index', [ + $response->setContent(p3k\XRay\view('index', [ 'title' => 'X-Ray' ])); return $response; diff --git a/controllers/Parse.php b/controllers/Parse.php index b29933f..d0be707 100644 --- a/controllers/Parse.php +++ b/controllers/Parse.php @@ -2,7 +2,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use XRay\Formats; +use p3k\XRay\Formats; class Parse { @@ -92,7 +92,7 @@ class Parse { ]); } - $url = \normalize_url($url); + $url = p3k\XRay\normalize_url($url); // Check if this is a Twitter URL and if they've provided API credentials, use the API if(preg_match('/https?:\/\/(?:mobile\.twitter\.com|twitter\.com|twtr\.io)\/(?:[a-z0-9_\/!#]+statuse?s?\/([0-9]+)|([a-zA-Z0-9_]+))/i', $url, $match)) { @@ -103,10 +103,14 @@ class Parse { return $this->parseGitHubURL($request, $response, $url); } - if(!should_follow_redirects($url)) + // Special-case appspot.com URLs to not follow redirects. + // https://cloud.google.com/appengine/docs/php/urlfetch/ + if(!p3k\XRay\should_follow_redirects($url)) { + $this->http->set_max_redirects(0); $this->http->set_transport(new p3k\HTTP\Stream()); - else + } else { $this->http->set_transport(new p3k\HTTP\Curl()); + } // Now fetch the URL and check for any curl errors // Don't cache the response if a token is used to fetch it diff --git a/controllers/Rels.php b/controllers/Rels.php index 8cdcef5..b4866b8 100644 --- a/controllers/Rels.php +++ b/controllers/Rels.php @@ -63,7 +63,7 @@ class Rels { ]); } - $url = \normalize_url($url); + $url = p3k\XRay\normalize_url($url); $result = $this->http->get($url); diff --git a/lib/XRay/Formats/GitHub.php b/lib/XRay/Formats/GitHub.php index cc1fb5d..766356d 100644 --- a/lib/XRay/Formats/GitHub.php +++ b/lib/XRay/Formats/GitHub.php @@ -1,5 +1,5 @@ 0) { // There is an author h-card on this page // Now look for the first h-* object other than an h-card and use that as the object @@ -496,7 +496,7 @@ class Mf2 { $found = false; foreach($item['properties']['url'] as $url) { if(self::isURL($url)) { - $url = self::normalize_url($url); + $url = \p3k\XRay\normalize_url($url); if($url == $authorURL) { $data['url'] = $url; $found = true; @@ -723,25 +723,4 @@ class Mf2 { } return \mf2\Parse($result['body'], $url); } - - private static function normalize_url($url) { - $parts = parse_url($url); - if(empty($parts['path'])) - $parts['path'] = '/'; - $parts['host'] = strtolower($parts['host']); - return self::build_url($parts); - } - - private static function build_url($parsed_url) { - $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : ''; - $host = isset($parsed_url['host']) ? $parsed_url['host'] : ''; - $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; - $user = isset($parsed_url['user']) ? $parsed_url['user'] : ''; - $pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : ''; - $pass = ($user || $pass) ? "$pass@" : ''; - $path = isset($parsed_url['path']) ? $parsed_url['path'] : ''; - $query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : ''; - $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : ''; - return "$scheme$user$pass$host$port$path$query$fragment"; - } } diff --git a/lib/XRay/Formats/Twitter.php b/lib/XRay/Formats/Twitter.php index 5bc1fe8..246d4d2 100644 --- a/lib/XRay/Formats/Twitter.php +++ b/lib/XRay/Formats/Twitter.php @@ -1,5 +1,5 @@ assertEquals('http://example.com/', $result); } public function testAddsSlashToBareDomain() { $url = 'http://example.com'; - $result = normalize_url($url); + $result = p3k\XRay\normalize_url($url); $this->assertEquals('http://example.com/', $result); } + public function testDoesNotModify() { + $url = 'https://example.com/'; + $result = p3k\XRay\normalize_url($url); + $this->assertEquals('https://example.com/', $result); + } + } From 00dbc3dae1f14bfef27ee3629b21033993ea1bd5 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Fri, 28 Apr 2017 13:00:18 -0700 Subject: [PATCH 07/17] relicense under MIT --- LICENSE.txt | 22 ++++++++++++++++++---- README.md | 2 ++ composer.json | 15 +++++++++------ composer.lock | 2 +- lib/helpers.php | 2 +- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 21ebae4..42310e7 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,7 +1,21 @@ -Copyright 2016 by Aaron Parecki +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at +Copyright (c) 2017 Aaron Parecki -http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index d1a9c52..1bd38e0 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ The contents of the URL is checked in the following order: * A silo URL from one of the following websites: ** Instagram ** Twitter +** GitHub +** XKCD ** (more coming soon) * h-entry, h-event, h-card diff --git a/composer.json b/composer.json index c0afae7..39f5f63 100644 --- a/composer.json +++ b/composer.json @@ -1,20 +1,18 @@ { "name": "p3k/xray", "type": "library", + "license": "MIT", + "homepage": "https://github.com/aaronpk/XRay", + "description": "X-Ray returns structured data from any URL", "require": { "mf2/mf2": "~0.3", "ezyang/htmlpurifier": "4.*", "indieweb/link-rel-parser": "0.1.*", "dg/twitter-php": "3.6.*", "p3k/timezone": "*", - "p3k/http": "*", + "p3k/http": "0.1.*", "cebe/markdown": "1.1.*" }, - "require-dev": { - "league/plates": "3.*", - "league/route": "1.*", - "phpunit/phpunit": "5.7.*" - }, "autoload": { "psr-4": { "p3k\\XRay\\": "lib/XRay" @@ -23,6 +21,11 @@ "lib/helpers.php" ] }, + "require-dev": { + "league/plates": "3.*", + "league/route": "1.*", + "phpunit/phpunit": "5.7.*" + }, "autoload-dev": { "files": [ "controllers/Main.php", diff --git a/composer.lock b/composer.lock index ca69224..9c02d81 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "10235592166c486bf7cf2601c9811861", + "content-hash": "051bf8d4c39d861dc9a22aeec3b79b57", "packages": [ { "name": "cebe/markdown", diff --git a/lib/helpers.php b/lib/helpers.php index 2f69df8..66c644f 100644 --- a/lib/helpers.php +++ b/lib/helpers.php @@ -35,4 +35,4 @@ function should_follow_redirects($url) { } else { return true; } -} \ No newline at end of file +} From 932cbedf456f6c404436fd14312777ae61be3619 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Fri, 28 Apr 2017 14:38:15 -0700 Subject: [PATCH 08/17] refactor Rels class into library and controller --- composer.json | 3 ++- controllers/Rels.php | 54 ++++++------------------------------- lib/XRay.php | 22 ++++++++++++++++ lib/XRay/Rels.php | 63 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 47 deletions(-) create mode 100644 lib/XRay.php create mode 100644 lib/XRay/Rels.php diff --git a/composer.json b/composer.json index 39f5f63..1eed498 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "p3k\\XRay\\": "lib/XRay" }, "files": [ - "lib/helpers.php" + "lib/helpers.php", + "lib/XRay.php" ] }, "require-dev": { diff --git a/controllers/Rels.php b/controllers/Rels.php index b4866b8..6dd5853 100644 --- a/controllers/Rels.php +++ b/controllers/Rels.php @@ -24,13 +24,15 @@ class Rels { } public function fetch(Request $request, Response $response) { + $opts = []; + if($request->get('timeout')) { // We might make 2 HTTP requests, so each request gets half the desired timeout - $this->http->timeout = $request->get('timeout') / 2; + $opts['timeout'] = $request->get('timeout') / 2; } if($request->get('max_redirects')) { - $this->http->max_redirects = (int)$request->get('max_redirects'); + $opts['max_redirects'] = (int)$request->get('max_redirects'); } if($request->get('pretty')) { @@ -46,51 +48,11 @@ class Rels { ]); } - // Attempt some basic URL validation - $scheme = parse_url($url, PHP_URL_SCHEME); - if(!in_array($scheme, ['http','https'])) { - return $this->respond($response, 400, [ - 'error' => 'invalid_url', - 'error_description' => 'Only http and https URLs are supported' - ]); - } - - $host = parse_url($url, PHP_URL_HOST); - if(!$host) { - return $this->respond($response, 400, [ - 'error' => 'invalid_url', - 'error_description' => 'The URL provided was not valid' - ]); - } - - $url = p3k\XRay\normalize_url($url); - - $result = $this->http->get($url); - - $html = $result['body']; - $mf2 = mf2\Parse($html, $result['url']); - - $rels = $result['rels']; - if(isset($mf2['rels'])) { - $rels = array_merge($rels, $mf2['rels']); - } - - // Resolve all relative URLs - foreach($rels as $rel=>$values) { - foreach($values as $i=>$value) { - $value = \mf2\resolveUrl($result['url'], $value); - $rels[$rel][$i] = $value; - } - } - - if(count($rels) == 0) - $rels = new StdClass; + $xray = new p3k\XRay(); + $xray->http = $this->http; + $res = $xray->rels($url, $opts); - return $this->respond($response, 200, [ - 'url' => $result['url'], - 'code' => $result['code'], - 'rels' => $rels - ]); + return $this->respond($response, !empty($res['error']) ? 400 : 200, $res); } } diff --git a/lib/XRay.php b/lib/XRay.php new file mode 100644 index 0000000..73cca7a --- /dev/null +++ b/lib/XRay.php @@ -0,0 +1,22 @@ +http = new HTTP(); + } + + public function rels($url, $opts=[]) { + $rels = new XRay\Rels($this->http); + return $rels->parse($url, $opts); + } + + public function parse($url, $opts=[]) { + $parser = new XRay\Parser($this->http); + return $parser->parse($url, $opts); + } + +} + diff --git a/lib/XRay/Rels.php b/lib/XRay/Rels.php new file mode 100644 index 0000000..b12301e --- /dev/null +++ b/lib/XRay/Rels.php @@ -0,0 +1,63 @@ +http = $http; + } + + public function parse($url, $opts=[]) { + if(isset($opts['timeout'])) + $this->http->set_timeout($opts['timeout']); + if(isset($opts['max_redirects'])) + $this->http->set_max_redirects($opts['max_redirects']); + + $scheme = parse_url($url, PHP_URL_SCHEME); + if(!in_array($scheme, ['http','https'])) { + return [ + 'error' => 'invalid_url', + 'error_description' => 'Only http and https URLs are supported' + ]; + } + + $host = parse_url($url, PHP_URL_HOST); + if(!$host) { + return [ + 'error' => 'invalid_url', + 'error_description' => 'The URL provided was not valid' + ]; + } + + $url = normalize_url($url); + + $result = $this->http->get($url); + + $html = $result['body']; + $mf2 = \mf2\Parse($html, $result['url']); + + $rels = $result['rels']; + if(isset($mf2['rels'])) { + $rels = array_merge($rels, $mf2['rels']); + } + + // Resolve all relative URLs + foreach($rels as $rel=>$values) { + foreach($values as $i=>$value) { + $value = \mf2\resolveUrl($result['url'], $value); + $rels[$rel][$i] = $value; + } + } + + if(count($rels) == 0) + $rels = new \StdClass; + + return [ + 'url' => $result['url'], + 'code' => $result['code'], + 'rels' => $rels + ]; + } + +} From 4014da6dc77795dba8024c867afbfdc6cc07e838 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sat, 29 Apr 2017 09:09:46 -0700 Subject: [PATCH 09/17] moves fetching logic into a library class --- controllers/Parse.php | 124 +++++--------------------- lib/XRay.php | 10 ++- lib/XRay/Fetch.php | 158 +++++++++++++++++++++++++++++++++ lib/XRay/Formats/Format.php | 36 ++++++++ lib/XRay/Formats/GitHub.php | 116 +++++++++++++++--------- lib/XRay/Formats/Instagram.php | 9 ++ lib/XRay/Formats/Twitter.php | 37 +++++++- lib/XRay/Formats/XKCD.php | 35 +++----- 8 files changed, 351 insertions(+), 174 deletions(-) create mode 100644 lib/XRay/Fetch.php create mode 100644 lib/XRay/Formats/Format.php diff --git a/controllers/Parse.php b/controllers/Parse.php index d0be707..eb35338 100644 --- a/controllers/Parse.php +++ b/controllers/Parse.php @@ -46,14 +46,15 @@ class Parse { } public function parse(Request $request, Response $response) { + $opts = []; if($request->get('timeout')) { // We might make 2 HTTP requests, so each request gets half the desired timeout - $this->http->set_timeout($request->get('timeout') / 2); + $opts['timeout'] = $request->get('timeout') / 2; } if($request->get('max_redirects') !== null) { - $this->http->set_max_redirects((int)$request->get('max_redirects')); + $opts['max_redirects'] = (int)$request->get('max_redirects'); } if($request->get('pretty')) { @@ -74,115 +75,30 @@ class Parse { // If HTML is provided in the request, parse that, and use the URL provided as the base URL for mf2 resolving $result['body'] = $html; $result['url'] = $url; + $result['code'] = null; } else { - // Attempt some basic URL validation - $scheme = parse_url($url, PHP_URL_SCHEME); - if(!in_array($scheme, ['http','https'])) { - return $this->respond($response, 400, [ - 'error' => 'invalid_url', - 'error_description' => 'Only http and https URLs are supported' - ]); - } - - $host = parse_url($url, PHP_URL_HOST); - if(!$host) { - return $this->respond($response, 400, [ - 'error' => 'invalid_url', - 'error_description' => 'The URL provided was not valid' - ]); - } - - $url = p3k\XRay\normalize_url($url); - - // Check if this is a Twitter URL and if they've provided API credentials, use the API - if(preg_match('/https?:\/\/(?:mobile\.twitter\.com|twitter\.com|twtr\.io)\/(?:[a-z0-9_\/!#]+statuse?s?\/([0-9]+)|([a-zA-Z0-9_]+))/i', $url, $match)) { - return $this->parseTwitterURL($request, $response, $url, $match); + $fetch = new p3k\XRay\Fetch($this->http); + + $fields = [ + 'twitter_api_key','twitter_api_secret','twitter_access_token','twitter_access_token_secret', + 'github_access_token', + 'token' + ]; + foreach($fields as $f) { + if($v=$request->get($f)) + $opts[$f] = $v; } - if($host == 'github.com') { - return $this->parseGitHubURL($request, $response, $url); - } + $result = $fetch->fetch($url, $opts); - // Special-case appspot.com URLs to not follow redirects. - // https://cloud.google.com/appengine/docs/php/urlfetch/ - if(!p3k\XRay\should_follow_redirects($url)) { - $this->http->set_max_redirects(0); - $this->http->set_transport(new p3k\HTTP\Stream()); - } else { - $this->http->set_transport(new p3k\HTTP\Curl()); - } - - // Now fetch the URL and check for any curl errors - // Don't cache the response if a token is used to fetch it - if($this->mc && !$request->get('token')) { - $cacheKey = 'xray-'.md5($url); - if($cached=$this->mc->get($cacheKey)) { - $result = json_decode($cached, true); - self::debug('using HTML from cache', 'X-Cache-Debug'); - } else { - $result = $this->http->get($url); - $cacheData = json_encode($result); - // App Engine limits the size of cached items, so don't cache ones larger than that - if(strlen($cacheData) < 1000000) - $this->mc->set($cacheKey, $cacheData, MEMCACHE_COMPRESSED, $this->_cacheTime); - } - } else { - $headers = []; - if($request->get('token')) { - $headers[] = 'Authorization: Bearer ' . $request->get('token'); - } - - $result = $this->http->get($url, $headers); - } - - if($result['error']) { - return $this->respond($response, 200, [ - 'error' => $result['error'], - 'error_description' => $result['error_description'], - 'url' => $result['url'], - 'code' => $result['code'] - ]); - } - - if(trim($result['body']) == '') { - if($result['code'] == 410) { - // 410 Gone responses are valid and should not return an error - return $this->respond($response, 200, [ - 'data' => [ - 'type' => 'unknown' - ], - 'url' => $result['url'], - 'code' => $result['code'] - ]); - } - - return $this->respond($response, 200, [ - 'error' => 'no_content', - 'error_description' => 'We did not get a response body when fetching the URL', - 'url' => $result['url'], - 'code' => $result['code'] - ]); + if(!empty($result['error'])) { + $error_code = isset($result['error_code']) ? $result['error_code'] : 200; + unset($result['error_code']); + return $this->respond($response, $error_code, $result); } + } - // Check for HTTP 401/403 - if($result['code'] == 401) { - return $this->respond($response, 200, [ - 'error' => 'unauthorized', - 'error_description' => 'The URL returned "HTTP 401 Unauthorized"', - 'url' => $result['url'], - 'code' => 401 - ]); - } - if($result['code'] == 403) { - return $this->respond($response, 200, [ - 'error' => 'forbidden', - 'error_description' => 'The URL returned "HTTP 403 Forbidden"', - 'url' => $result['url'], - 'code' => 403 - ]); - } - } // Check for known services $host = parse_url($result['url'], PHP_URL_HOST); diff --git a/lib/XRay.php b/lib/XRay.php index 73cca7a..f633045 100644 --- a/lib/XRay.php +++ b/lib/XRay.php @@ -14,8 +14,14 @@ class XRay { } public function parse($url, $opts=[]) { - $parser = new XRay\Parser($this->http); - return $parser->parse($url, $opts); + $fetch = new XRay\Fetch($this->http); + $response = $fetch->fetch($url, $opts); + return $this->parse_doc($response, $url, $opts); + } + + public function parse_doc($response, $url=false, $opts=[]) { + + } } diff --git a/lib/XRay/Fetch.php b/lib/XRay/Fetch.php new file mode 100644 index 0000000..dda839f --- /dev/null +++ b/lib/XRay/Fetch.php @@ -0,0 +1,158 @@ +http = $http; + } + + public function fetch($url, $opts=[]) { + if(isset($opts['timeout'])) + $this->http->set_timeout($opts['timeout']); + if(isset($opts['max_redirects'])) + $this->http->set_max_redirects($opts['max_redirects']); + + // Attempt some basic URL validation + $scheme = parse_url($url, PHP_URL_SCHEME); + if(!in_array($scheme, ['http','https'])) { + return [ + 'error_code' => 400, + 'error' => 'invalid_url', + 'error_description' => 'Only http and https URLs are supported' + ]; + } + + $host = parse_url($url, PHP_URL_HOST); + if(!$host) { + return [ + 'error_code' => 400, + 'error' => 'invalid_url', + 'error_description' => 'The URL provided was not valid' + ]; + } + + $url = normalize_url($url); + $host = parse_url($url, PHP_URL_HOST); + + // Check if this is a Twitter URL and if they've provided API credentials, use the API + if(Formats\Twitter::matches_host($url)) { + return $this->_fetch_tweet($url, $opts); + } + + if(Formats\GitHub::matches_host($url)) { + return $this->_fetch_github($url, $opts); + } + + // Special-case appspot.com URLs to not follow redirects. + // https://cloud.google.com/appengine/docs/php/urlfetch/ + if(!should_follow_redirects($url)) { + $this->http->set_max_redirects(0); + $this->http->set_transport(new \p3k\HTTP\Stream()); + } else { + $this->http->set_transport(new \p3k\HTTP\Curl()); + } + + $headers = []; + if(isset($opts['token'])) + $headers[] = 'Authorization: Bearer ' . $opts['token']; + + $result = $this->http->get($url, $headers); + + if($result['error']) { + return [ + 'error' => $result['error'], + 'error_description' => $result['error_description'], + 'url' => $result['url'], + 'code' => $result['code'], + ]; + } + + if(trim($result['body']) == '') { + if($result['code'] == 410) { + // 410 Gone responses are valid and should not return an error + return $this->respond($response, 200, [ + 'TODO' => [ + ], + 'url' => $result['url'], + 'code' => $result['code'] + ]); + } + + return [ + 'error' => 'no_content', + 'error_description' => 'We did not get a response body when fetching the URL', + 'url' => $result['url'], + 'code' => $result['code'] + ]; + } + + // Check for HTTP 401/403 + if($result['code'] == 401) { + return [ + 'error' => 'unauthorized', + 'error_description' => 'The URL returned "HTTP 401 Unauthorized"', + 'url' => $result['url'], + 'code' => $result['code'] + ]; + } + if($result['code'] == 403) { + return [ + 'error' => 'forbidden', + 'error_description' => 'The URL returned "HTTP 403 Forbidden"', + 'url' => $result['url'], + 'code' => $result['code'] + ]; + } + + return [ + 'url' => $result['url'], + 'body' => $result['body'], + 'code' => $result['code'], + ]; + } + + private function _fetch_tweet($url, $opts) { + $fields = ['twitter_api_key','twitter_api_secret','twitter_access_token','twitter_access_token_secret']; + $creds = []; + foreach($fields as $f) { + if(isset($opts[$f])) + $creds[$f] = $opts[$f]; + } + + if(count($creds) < 4) { + return [ + 'error_code' => 400, + 'error' => 'missing_parameters', + 'error_description' => 'All 4 Twitter credentials must be included in the request' + ]; + } + + $tweet = Formats\Twitter::fetch($url, $creds); + if(!$tweet) { + return [ + 'error' => 'twitter_error', + 'error_description' => $e->getMessage() + ]; + } + + return [ + 'url' => $url, + 'body' => $tweet, + 'code' => 200, + ]; + } + + private function _fetch_github($url, $opts) { + $fields = ['github_access_token']; + $creds = []; + foreach($fields as $f) { + if(isset($opts[$f])) + $creds[$f] = $opts[$f]; + } + + return Formats\GitHub::fetch($this->http, $url, $creds); + } + +} diff --git a/lib/XRay/Formats/Format.php b/lib/XRay/Formats/Format.php new file mode 100644 index 0000000..47e9625 --- /dev/null +++ b/lib/XRay/Formats/Format.php @@ -0,0 +1,36 @@ + [ + 'type' => 'unknown' + ] + ]; + } + + protected static function _loadHTML($html) { + $doc = new DOMDocument(); + @$doc->loadHTML($html); + + if(!$doc) { + return [null, null]; + } + + $xpath = new DOMXPath($doc); + + return [$doc, $xpath]; + } + +} diff --git a/lib/XRay/Formats/GitHub.php b/lib/XRay/Formats/GitHub.php index 766356d..91c8e96 100644 --- a/lib/XRay/Formats/GitHub.php +++ b/lib/XRay/Formats/GitHub.php @@ -2,53 +2,85 @@ namespace p3k\XRay\Formats; use DateTime, DateTimeZone; -use Parse, Config; +use Config; use cebe\markdown\GithubMarkdown; -class GitHub { +class GitHub extends Format { + + public static function matches_host($url) { + $host = parse_url($url, PHP_URL_HOST); + return $host == 'github.com'; + } + + public static function matches($url) { + return preg_match('~https://github.com/([^/]+)/([^/]+)/pull/(\d+)$~', $url, $match) + || preg_match('~https://github.com/([^/]+)/([^/]+)/issues/(\d+)$~', $url, $match) + || preg_match('~https://github.com/([^/]+)/([^/]+)$~', $url, $match) + || preg_match('~https://github.com/([^/]+)/([^/]+)/issues/(\d+)#issuecomment-(\d+)~', $url, $match); + } + + public static function fetch($http, $url, $creds) { + // Transform the GitHub URL to an API request + if(preg_match('~https://github.com/([^/]+)/([^/]+)/pull/(\d+)$~', $url, $match)) { + $type = 'pull'; + $org = $match[1]; + $repo = $match[2]; + $pull = $match[3]; + $apiurl = 'https://api.github.com/repos/'.$org.'/'.$repo.'/pulls/'.$pull; + + } elseif(preg_match('~https://github.com/([^/]+)/([^/]+)/issues/(\d+)$~', $url, $match)) { + $type = 'issue'; + $org = $match[1]; + $repo = $match[2]; + $issue = $match[3]; + $apiurl = 'https://api.github.com/repos/'.$org.'/'.$repo.'/issues/'.$issue; + + } elseif(preg_match('~https://github.com/([^/]+)/([^/]+)$~', $url, $match)) { + $type = 'repo'; + $org = $match[1]; + $repo = $match[2]; + $apiurl = 'https://api.github.com/repos/'.$org.'/'.$repo; + + } elseif(preg_match('~https://github.com/([^/]+)/([^/]+)/issues/(\d+)#issuecomment-(\d+)~', $url, $match)) { + $type = 'comment'; + $org = $match[1]; + $repo = $match[2]; + $issue = $match[3]; + $comment = $match[4]; + $apiurl = 'https://api.github.com/repos/'.$org.'/'.$repo.'/issues/comments/'.$comment; + + } else { + return [ + 'error' => 'unsupported_url', + 'error_description' => 'This GitHub URL is not supported', + 'error_code' => 400, + ]; + } + + $headers = []; + if(isset($creds['github_access_token'])) { + $headers[] = 'Authorization: Bearer ' . $creds['github_access_token']; + } + + $response = $http->get($apiurl, $headers); + if($response['code'] != 200) { + return [ + 'error' => 'github_error', + 'error_description' => $response['body'], + 'code' => $response['code'], + ]; + } + + return [ + 'url' => $url, + 'body' => $response['body'], + 'code' => $response['code'], + ]; + } public static function parse($http, $url, $creds, $json=null) { - if(!$json) { - // Transform the GitHub URL to an API request - if(preg_match('~https://github.com/([^/]+)/([^/]+)/pull/(\d+)$~', $url, $match)) { - $type = 'pull'; - $org = $match[1]; - $repo = $match[2]; - $pull = $match[3]; - $apiurl = 'https://api.github.com/repos/'.$org.'/'.$repo.'/pulls/'.$pull; - - } elseif(preg_match('~https://github.com/([^/]+)/([^/]+)/issues/(\d+)$~', $url, $match)) { - $type = 'issue'; - $org = $match[1]; - $repo = $match[2]; - $issue = $match[3]; - $apiurl = 'https://api.github.com/repos/'.$org.'/'.$repo.'/issues/'.$issue; - - } elseif(preg_match('~https://github.com/([^/]+)/([^/]+)$~', $url, $match)) { - $type = 'repo'; - $org = $match[1]; - $repo = $match[2]; - $apiurl = 'https://api.github.com/repos/'.$org.'/'.$repo; - - } elseif(preg_match('~https://github.com/([^/]+)/([^/]+)/issues/(\d+)#issuecomment-(\d+)~', $url, $match)) { - $type = 'comment'; - $org = $match[1]; - $repo = $match[2]; - $issue = $match[3]; - $comment = $match[4]; - $apiurl = 'https://api.github.com/repos/'.$org.'/'.$repo.'/issues/comments/'.$comment; - - } else { - return [null, null, 0]; - } - - $response = $http->get($apiurl, ['User-Agent: XRay ('.Config::$base.')']); - if($response['code'] != 200) { - return [null, $response['body'], $response['code']]; - } - - $data = json_decode($response['body'], true); + if(false) { } else { $data = json_decode($json, true); } diff --git a/lib/XRay/Formats/Instagram.php b/lib/XRay/Formats/Instagram.php index f49ff15..1cfaee5 100644 --- a/lib/XRay/Formats/Instagram.php +++ b/lib/XRay/Formats/Instagram.php @@ -7,6 +7,15 @@ use Parse; class Instagram { + public static function matches_host($url) { + $host = parse_url($url, PHP_URL_HOST); + return in_array($host, ['www.instagram.com','instagram.com']); + } + + public static function matches($url) { + return self::matches_host($url); + } + public static function parse($html, $url, $http) { $photoData = self::_extractPhotoDataFromPhotoPage($html); diff --git a/lib/XRay/Formats/Twitter.php b/lib/XRay/Formats/Twitter.php index 246d4d2..905f24c 100644 --- a/lib/XRay/Formats/Twitter.php +++ b/lib/XRay/Formats/Twitter.php @@ -2,9 +2,42 @@ namespace p3k\XRay\Formats; use DateTime, DateTimeZone; -use Parse; -class Twitter { +class Twitter extends Format { + + public static function matches_host($url) { + $host = parse_url($url, PHP_URL_HOST); + return in_array($host, ['mobile.twitter.com','twitter.com','www.twitter.com','twtr.io']); + } + + public static function matches($url) { + if(preg_match('/https?:\/\/(?:mobile\.twitter\.com|twitter\.com|twtr\.io)\/(?:[a-z0-9_\/!#]+statuse?s?\/([0-9]+)|([a-zA-Z0-9_]+))/i', $url, $match)) + return $match; + else + return false; + } + + public static function fetch($url, $creds) { + if(!($match = self::matches($url))) { + return false; + } + + $tweet_id = $match[1]; + + $host = parse_url($url, PHP_URL_HOST); + if($host == 'twtr.io') { + $tweet_id = self::b60to10($tweet_id); + } + + $twitter = new \Twitter($creds['twitter_api_key'], $creds['twitter_api_secret'], $creds['twitter_access_token'], $creds['twitter_access_token_secret']); + try { + $tweet = $twitter->request('statuses/show/'.$tweet_id, 'GET', ['tweet_mode'=>'extended']); + } catch(\TwitterException $e) { + return false; + } + + return $tweet; + } public static function parse($url, $tweet_id, $creds, $json=null) { diff --git a/lib/XRay/Formats/XKCD.php b/lib/XRay/Formats/XKCD.php index acdb6e8..d7dc687 100644 --- a/lib/XRay/Formats/XKCD.php +++ b/lib/XRay/Formats/XKCD.php @@ -1,11 +1,19 @@ [ - 'type' => 'unknown' - ] - ]; - } - - private static function _loadHTML($html) { - $doc = new DOMDocument(); - @$doc->loadHTML($html); - - if(!$doc) { - return [null, null]; - } - - $xpath = new DOMXPath($doc); - - return [$doc, $xpath]; - } - } From 32df010cbdd9b5daeaf2188bcaf471b9f651ed1b Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sat, 29 Apr 2017 09:11:26 -0700 Subject: [PATCH 10/17] update readme --- README.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1bd38e0..e525995 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,18 @@ XRay parses structured content from a URL. The contents of the URL is checked in the following order: * A silo URL from one of the following websites: -** Instagram -** Twitter -** GitHub -** XKCD -** (more coming soon) -* h-entry, h-event, h-card - + * Instagram + * Twitter + * GitHub + * XKCD + * (more coming soon) +* Microformats + * h-card + * h-entry + * h-event + * h-review + * h-recipe + * h-product ## Parse API From 7a7fa6cabbd6e6ac73f3170a2e6bfa12c4e009ac Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sat, 29 Apr 2017 09:12:42 -0700 Subject: [PATCH 11/17] markdown formatting --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e525995..a048ffd 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,18 @@ XRay parses structured content from a URL. The contents of the URL is checked in the following order: * A silo URL from one of the following websites: - * Instagram - * Twitter - * GitHub - * XKCD - * (more coming soon) + * Instagram + * Twitter + * GitHub + * XKCD + * (more coming soon) * Microformats - * h-card - * h-entry - * h-event - * h-review - * h-recipe - * h-product + * h-card + * h-entry + * h-event + * h-review + * h-recipe + * h-product ## Parse API From 2f52eba556e4542f829854c41821f4ac7c97b9c5 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sat, 29 Apr 2017 09:31:54 -0700 Subject: [PATCH 12/17] rename Fetcher class, add stub Parser class --- controllers/Parse.php | 4 ++-- lib/XRay/{Fetch.php => Fetcher.php} | 7 +++++-- lib/XRay/Parser.php | 12 ++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) rename lib/XRay/{Fetch.php => Fetcher.php} (95%) create mode 100644 lib/XRay/Parser.php diff --git a/controllers/Parse.php b/controllers/Parse.php index eb35338..485de53 100644 --- a/controllers/Parse.php +++ b/controllers/Parse.php @@ -77,7 +77,7 @@ class Parse { $result['url'] = $url; $result['code'] = null; } else { - $fetch = new p3k\XRay\Fetch($this->http); + $fetcher = new p3k\XRay\Fetcher($this->http); $fields = [ 'twitter_api_key','twitter_api_secret','twitter_access_token','twitter_access_token_secret', @@ -89,7 +89,7 @@ class Parse { $opts[$f] = $v; } - $result = $fetch->fetch($url, $opts); + $result = $fetcher->fetch($url, $opts); if(!empty($result['error'])) { $error_code = isset($result['error_code']) ? $result['error_code'] : 200; diff --git a/lib/XRay/Fetch.php b/lib/XRay/Fetcher.php similarity index 95% rename from lib/XRay/Fetch.php rename to lib/XRay/Fetcher.php index dda839f..ea37f8b 100644 --- a/lib/XRay/Fetch.php +++ b/lib/XRay/Fetcher.php @@ -1,7 +1,7 @@ _fetch_tweet($url, $opts); } + // Transform the HTML GitHub URL into an GitHub API request and fetch the API response if(Formats\GitHub::matches_host($url)) { return $this->_fetch_github($url, $opts); } + // All other URLs are fetched normally + // Special-case appspot.com URLs to not follow redirects. // https://cloud.google.com/appengine/docs/php/urlfetch/ if(!should_follow_redirects($url)) { diff --git a/lib/XRay/Parser.php b/lib/XRay/Parser.php new file mode 100644 index 0000000..d39369c --- /dev/null +++ b/lib/XRay/Parser.php @@ -0,0 +1,12 @@ + Date: Sat, 29 Apr 2017 09:32:02 -0700 Subject: [PATCH 13/17] use phpunit 4.8 for php 5.5 support --- composer.json | 2 +- composer.lock | 341 +++++++++++--------------------------------------- 2 files changed, 73 insertions(+), 270 deletions(-) diff --git a/composer.json b/composer.json index 1eed498..b84ad4a 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "require-dev": { "league/plates": "3.*", "league/route": "1.*", - "phpunit/phpunit": "5.7.*" + "phpunit/phpunit": "4.8.*" }, "autoload-dev": { "files": [ diff --git a/composer.lock b/composer.lock index 9c02d81..645eae6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "051bf8d4c39d861dc9a22aeec3b79b57", + "content-hash": "7bbd363fe763d3cd91781c2f335cda19", "packages": [ { "name": "cebe/markdown", @@ -596,48 +596,6 @@ ], "time": "2015-09-11T07:40:31+00:00" }, - { - "name": "myclabs/deep-copy", - "version": "1.6.1", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102", - "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "doctrine/collections": "1.*", - "phpunit/phpunit": "~4.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "homepage": "https://github.com/myclabs/DeepCopy", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "time": "2017-04-12T18:52:22+00:00" - }, { "name": "nikic/fast-route", "version": "v0.8.0", @@ -892,40 +850,39 @@ }, { "name": "phpunit/php-code-coverage", - "version": "4.0.8", + "version": "2.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-xmlwriter": "*", - "php": "^5.6 || ^7.0", - "phpunit/php-file-iterator": "^1.3", - "phpunit/php-text-template": "^1.2", - "phpunit/php-token-stream": "^1.4.2 || ^2.0", - "sebastian/code-unit-reverse-lookup": "^1.0", - "sebastian/environment": "^1.3.2 || ^2.0", - "sebastian/version": "^1.0 || ^2.0" + "php": ">=5.3.3", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.2", + "phpunit/php-token-stream": "~1.3", + "sebastian/environment": "^1.3.2", + "sebastian/version": "~1.0" }, "require-dev": { - "ext-xdebug": "^2.1.4", - "phpunit/phpunit": "^5.7" + "ext-xdebug": ">=2.1.4", + "phpunit/phpunit": "~4" }, "suggest": { - "ext-xdebug": "^2.5.1" + "ext-dom": "*", + "ext-xdebug": ">=2.2.1", + "ext-xmlwriter": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0.x-dev" + "dev-master": "2.2.x-dev" } }, "autoload": { @@ -951,7 +908,7 @@ "testing", "xunit" ], - "time": "2017-04-02T07:44:40+00:00" + "time": "2015-10-06T15:47:00+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1141,50 +1098,40 @@ }, { "name": "phpunit/phpunit", - "version": "5.7.19", + "version": "4.8.35", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1" + "reference": "791b1a67c25af50e230f841ee7a9c6eba507dc87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/69c4f49ff376af2692bad9cebd883d17ebaa98a1", - "reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/791b1a67c25af50e230f841ee7a9c6eba507dc87", + "reference": "791b1a67c25af50e230f841ee7a9c6eba507dc87", "shasum": "" }, "require": { "ext-dom": "*", "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "myclabs/deep-copy": "~1.3", - "php": "^5.6 || ^7.0", - "phpspec/prophecy": "^1.6.2", - "phpunit/php-code-coverage": "^4.0.4", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.3.3", + "phpspec/prophecy": "^1.3.1", + "phpunit/php-code-coverage": "~2.1", "phpunit/php-file-iterator": "~1.4", "phpunit/php-text-template": "~1.2", "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "^3.2", - "sebastian/comparator": "^1.2.4", + "phpunit/phpunit-mock-objects": "~2.3", + "sebastian/comparator": "~1.2.2", "sebastian/diff": "~1.2", - "sebastian/environment": "^1.3.4 || ^2.0", - "sebastian/exporter": "~2.0", - "sebastian/global-state": "^1.1", - "sebastian/object-enumerator": "~2.0", - "sebastian/resource-operations": "~1.0", - "sebastian/version": "~1.0.3|~2.0", + "sebastian/environment": "~1.3", + "sebastian/exporter": "~1.2", + "sebastian/global-state": "~1.0", + "sebastian/version": "~1.0", "symfony/yaml": "~2.1|~3.0" }, - "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2" - }, - "require-dev": { - "ext-pdo": "*" - }, "suggest": { - "ext-xdebug": "*", "phpunit/php-invoker": "~1.1" }, "bin": [ @@ -1193,7 +1140,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.7.x-dev" + "dev-master": "4.8.x-dev" } }, "autoload": { @@ -1219,33 +1166,30 @@ "testing", "xunit" ], - "time": "2017-04-03T02:22:27+00:00" + "time": "2017-02-06T05:18:07+00:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "3.4.3", + "version": "2.3.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24" + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", - "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", - "php": "^5.6 || ^7.0", - "phpunit/php-text-template": "^1.2", - "sebastian/exporter": "^1.2 || ^2.0" - }, - "conflict": { - "phpunit/phpunit": "<5.4.0" + "php": ">=5.3.3", + "phpunit/php-text-template": "~1.2", + "sebastian/exporter": "~1.2" }, "require-dev": { - "phpunit/phpunit": "^5.4" + "phpunit/phpunit": "~4.4" }, "suggest": { "ext-soap": "*" @@ -1253,7 +1197,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2.x-dev" + "dev-master": "2.3.x-dev" } }, "autoload": { @@ -1278,52 +1222,7 @@ "mock", "xunit" ], - "time": "2016-12-08T20:27:08+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2017-03-04T06:30:41+00:00" + "time": "2015-10-02T06:51:40+00:00" }, { "name": "sebastian/comparator", @@ -1443,28 +1342,28 @@ }, { "name": "sebastian/environment", - "version": "2.0.0", + "version": "1.3.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" + "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", + "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^5.3.3 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^5.0" + "phpunit/phpunit": "^4.8 || ^5.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.3.x-dev" } }, "autoload": { @@ -1489,25 +1388,25 @@ "environment", "hhvm" ], - "time": "2016-11-26T07:53:53+00:00" + "time": "2016-08-18T05:49:44+00:00" }, { "name": "sebastian/exporter", - "version": "2.0.0", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4" + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", - "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", "shasum": "" }, "require": { "php": ">=5.3.3", - "sebastian/recursion-context": "~2.0" + "sebastian/recursion-context": "~1.0" }, "require-dev": { "ext-mbstring": "*", @@ -1516,7 +1415,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.3.x-dev" } }, "autoload": { @@ -1556,7 +1455,7 @@ "export", "exporter" ], - "time": "2016-11-19T08:54:04+00:00" + "time": "2016-06-17T09:04:28+00:00" }, { "name": "sebastian/global-state", @@ -1609,64 +1508,18 @@ ], "time": "2015-10-12T03:26:01+00:00" }, - { - "name": "sebastian/object-enumerator", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1311872ac850040a79c3c058bea3e22d0f09cbb7", - "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7", - "shasum": "" - }, - "require": { - "php": ">=5.6", - "sebastian/recursion-context": "~2.0" - }, - "require-dev": { - "phpunit/phpunit": "~5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2017-02-18T15:18:39+00:00" - }, { "name": "sebastian/recursion-context", - "version": "2.0.0", + "version": "1.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a" + "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a", - "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/b19cc3298482a335a95f3016d2f8a6950f0fbcd7", + "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7", "shasum": "" }, "require": { @@ -1678,7 +1531,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { @@ -1706,73 +1559,23 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2016-11-19T07:33:16+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "shasum": "" - }, - "require": { - "php": ">=5.6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2015-07-28T20:34:47+00:00" + "time": "2016-10-03T07:41:43+00:00" }, { "name": "sebastian/version", - "version": "2.0.1", + "version": "1.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", "shasum": "" }, - "require": { - "php": ">=5.6" - }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, "autoload": { "classmap": [ "src/" @@ -1791,7 +1594,7 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "time": "2016-10-03T07:35:21+00:00" + "time": "2015-06-21T13:59:46+00:00" }, { "name": "symfony/http-foundation", From 6b65ae1b94db9b49242ea2544071c781a839ff7c Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sat, 29 Apr 2017 10:33:27 -0700 Subject: [PATCH 14/17] refactor for parsing Instagram and GitHub complete --- controllers/Parse.php | 17 ++++++ lib/XRay.php | 30 ++++++--- lib/XRay/Fetcher.php | 2 + lib/XRay/Formats/GitHub.php | 108 ++++++++++++++++++--------------- lib/XRay/Formats/Instagram.php | 26 ++++---- lib/XRay/Parser.php | 30 ++++++++- tests/InstagramTest.php | 8 +-- 7 files changed, 145 insertions(+), 76 deletions(-) diff --git a/controllers/Parse.php b/controllers/Parse.php index 485de53..b2cd612 100644 --- a/controllers/Parse.php +++ b/controllers/Parse.php @@ -98,6 +98,23 @@ class Parse { } } + $parser = new p3k\XRay\Parser($this->http); + $parsed = $parser->parse($result['body'], $result['url'], $opts); + + // Allow the parser to override the HTTP response code, e.g. a meta-equiv tag + if(isset($parsed['code'])) + $result['code'] = $parsed['code']; + + $data = [ + 'data' => $parsed['data'], + 'url' => $result['url'], + 'code' => $result['code'] + ]; + if($request->get('include_original') && isset($parsed['original'])) + $data['original'] = $parsed['original']; + + return $this->respond($response, 200, $data); + // Check for known services diff --git a/lib/XRay.php b/lib/XRay.php index f633045..7fe7192 100644 --- a/lib/XRay.php +++ b/lib/XRay.php @@ -13,15 +13,29 @@ class XRay { return $rels->parse($url, $opts); } - public function parse($url, $opts=[]) { - $fetch = new XRay\Fetch($this->http); - $response = $fetch->fetch($url, $opts); - return $this->parse_doc($response, $url, $opts); - } + public function parse($url, $opts_or_body=false, $opts_for_body=[]) { + if(!$opts_or_body || is_array($opts_or_body)) { + $fetch = new XRay\Fetcher($this->http); + $response = $fetch->fetch($url, $opts_or_body); + if(!empty($response['error'])) + return $response; + $body = $response['body']; + $url = $response['url']; + $code = $response['code']; + $opts = is_array($opts_or_body) ? $opts_or_body : $opts_for_body; + } else { + $body = $opts_or_body; + $opts = $opts_for_body; + $code = null; + } + $parser = new XRay\Parser($this->http); - public function parse_doc($response, $url=false, $opts=[]) { - - + $result = $parser->parse($body, $url, $opts); + if(!isset($opts['include_original']) || !$opts['include_original']) + unset($result['original']); + $result['url'] = $url; + $result['code'] = isset($result['code']) ? $result['code'] : $code; + return $result; } } diff --git a/lib/XRay/Fetcher.php b/lib/XRay/Fetcher.php index ea37f8b..9b82909 100644 --- a/lib/XRay/Fetcher.php +++ b/lib/XRay/Fetcher.php @@ -9,6 +9,8 @@ class Fetcher { } public function fetch($url, $opts=[]) { + if($opts == false) $opts = []; + if(isset($opts['timeout'])) $this->http->set_timeout($opts['timeout']); if(isset($opts['max_redirects'])) diff --git a/lib/XRay/Formats/GitHub.php b/lib/XRay/Formats/GitHub.php index 91c8e96..f55c184 100644 --- a/lib/XRay/Formats/GitHub.php +++ b/lib/XRay/Formats/GitHub.php @@ -19,37 +19,50 @@ class GitHub extends Format { || preg_match('~https://github.com/([^/]+)/([^/]+)/issues/(\d+)#issuecomment-(\d+)~', $url, $match); } - public static function fetch($http, $url, $creds) { - // Transform the GitHub URL to an API request + private static function extract_url_parts($url) { + $response = false; + if(preg_match('~https://github.com/([^/]+)/([^/]+)/pull/(\d+)$~', $url, $match)) { - $type = 'pull'; - $org = $match[1]; - $repo = $match[2]; - $pull = $match[3]; - $apiurl = 'https://api.github.com/repos/'.$org.'/'.$repo.'/pulls/'.$pull; + $response = []; + $response['type'] = 'pull'; + $response['org'] = $match[1]; + $response['repo'] = $match[2]; + $response['pull'] = $match[3]; + $response['apiurl'] = 'https://api.github.com/repos/'.$response['org'].'/'.$response['repo'].'/pulls/'.$response['pull']; } elseif(preg_match('~https://github.com/([^/]+)/([^/]+)/issues/(\d+)$~', $url, $match)) { - $type = 'issue'; - $org = $match[1]; - $repo = $match[2]; - $issue = $match[3]; - $apiurl = 'https://api.github.com/repos/'.$org.'/'.$repo.'/issues/'.$issue; + $response = []; + $response['type'] = 'issue'; + $response['org'] = $match[1]; + $response['repo'] = $match[2]; + $response['issue'] = $match[3]; + $response['apiurl'] = 'https://api.github.com/repos/'.$response['org'].'/'.$response['repo'].'/issues/'.$response['issue']; } elseif(preg_match('~https://github.com/([^/]+)/([^/]+)$~', $url, $match)) { - $type = 'repo'; - $org = $match[1]; - $repo = $match[2]; - $apiurl = 'https://api.github.com/repos/'.$org.'/'.$repo; + $response = []; + $response['type'] = 'repo'; + $response['org'] = $match[1]; + $response['repo'] = $match[2]; + $response['apiurl'] = 'https://api.github.com/repos/'.$response['org'].'/'.$response['repo']; } elseif(preg_match('~https://github.com/([^/]+)/([^/]+)/issues/(\d+)#issuecomment-(\d+)~', $url, $match)) { - $type = 'comment'; - $org = $match[1]; - $repo = $match[2]; - $issue = $match[3]; - $comment = $match[4]; - $apiurl = 'https://api.github.com/repos/'.$org.'/'.$repo.'/issues/comments/'.$comment; - - } else { + $response = []; + $response['type'] = 'comment'; + $response['org'] = $match[1]; + $response['repo'] = $match[2]; + $response['issue'] = $match[3]; + $response['comment'] = $match[4]; + $response['apiurl'] = 'https://api.github.com/repos/'.$response['org'].'/'.$response['repo'].'/issues/comments/'.$response['comment']; + + } + + return $response; + } + + public static function fetch($http, $url, $creds) { + $parts = self::extract_url_parts($url); + + if(!$parts) { return [ 'error' => 'unsupported_url', 'error_description' => 'This GitHub URL is not supported', @@ -62,7 +75,7 @@ class GitHub extends Format { $headers[] = 'Authorization: Bearer ' . $creds['github_access_token']; } - $response = $http->get($apiurl, $headers); + $response = $http->get($parts['apiurl'], $headers); if($response['code'] != 200) { return [ 'error' => 'github_error', @@ -78,20 +91,20 @@ class GitHub extends Format { ]; } - public static function parse($http, $url, $creds, $json=null) { + public static function parse($json, $url) { + $data = @json_decode($json, true); - if(false) { - } else { - $data = json_decode($json, true); - } + if(!$data) + return self::_unknown(); - if(!$data) { - return [null, null, 0]; - } + $parts = self::extract_url_parts($url); + + if(!$parts) + return self::_unknown(); // Start building the h-entry $entry = array( - 'type' => ($type == 'repo' ? 'repo' : 'entry'), + 'type' => ($parts['type'] == 'repo' ? 'repo' : 'entry'), 'url' => $url, 'author' => [ 'type' => 'card', @@ -101,7 +114,7 @@ class GitHub extends Format { ] ); - if($type == 'repo') + if($parts['type'] == 'repo') $authorkey = 'owner'; else $authorkey = 'user'; @@ -110,20 +123,20 @@ class GitHub extends Format { $entry['author']['photo'] = $data[$authorkey]['avatar_url']; $entry['author']['url'] = $data[$authorkey]['html_url']; - if($type == 'pull') { - $entry['name'] = '#' . $pull . ' ' . $data['title']; - } elseif($type == 'issue') { - $entry['name'] = '#' . $issue . ' ' . $data['title']; - } elseif($type == 'repo') { + if($parts['type'] == 'pull') { + $entry['name'] = '#' . $parts['pull'] . ' ' . $data['title']; + } elseif($parts['type'] == 'issue') { + $entry['name'] = '#' . $parts['issue'] . ' ' . $data['title']; + } elseif($parts['type'] == 'repo') { $entry['name'] = $data['name']; } - if($type == 'repo') { + if($parts['type'] == 'repo') { if(!empty($data['description'])) $entry['summary'] = $data['description']; } - if($type != 'repo' && !empty($data['body'])) { + if($parts['type'] != 'repo' && !empty($data['body'])) { $parser = new GithubMarkdown(); $entry['content'] = [ @@ -132,8 +145,8 @@ class GitHub extends Format { ]; } - if($type == 'comment') { - $entry['in-reply-to'] = ['https://github.com/'.$org.'/'.$repo.'/issues/'.$issue]; + if($parts['type'] == 'comment') { + $entry['in-reply-to'] = ['https://github.com/'.$parts['org'].'/'.$parts['repo'].'/issues/'.$parts['issue']]; } if(!empty($data['labels'])) { @@ -144,11 +157,10 @@ class GitHub extends Format { $entry['published'] = $data['created_at']; - $r = [ - 'data' => $entry + return [ + 'data' => $entry, + 'original' => $json ]; - - return [$r, $json, $response['code']]; } } diff --git a/lib/XRay/Formats/Instagram.php b/lib/XRay/Formats/Instagram.php index 1cfaee5..2c02b51 100644 --- a/lib/XRay/Formats/Instagram.php +++ b/lib/XRay/Formats/Instagram.php @@ -3,9 +3,8 @@ namespace p3k\XRay\Formats; use DOMDocument, DOMXPath; use DateTime, DateTimeZone; -use Parse; -class Instagram { +class Instagram extends Format { public static function matches_host($url) { $host = parse_url($url, PHP_URL_HOST); @@ -16,12 +15,12 @@ class Instagram { return self::matches_host($url); } - public static function parse($html, $url, $http) { + public static function parse($http, $html, $url) { $photoData = self::_extractPhotoDataFromPhotoPage($html); if(!$photoData) - return false; + return self::_unknown(); // Start building the h-entry $entry = array( @@ -140,19 +139,18 @@ class Instagram { $entry['published'] = $published->format('c'); - $response = [ - 'data' => $entry - ]; - if(count($refs)) { - $response['refs'] = $refs; + $entry['refs'] = $refs; } - return [$response, [ - 'photo' => $photoData, - 'profiles' => $profiles, - 'locations' => $locations - ]]; + return [ + 'data' => $entry, + 'original' => json_encode([ + 'photo' => $photoData, + 'profiles' => $profiles, + 'locations' => $locations + ]) + ]; } private static function _buildHCardFromInstagramProfile($profile) { diff --git a/lib/XRay/Parser.php b/lib/XRay/Parser.php index d39369c..97fab1f 100644 --- a/lib/XRay/Parser.php +++ b/lib/XRay/Parser.php @@ -1,12 +1,38 @@ http = $http; + } - public function parse($url, $body) { + public function parse($body, $url, $opts=[]) { + if(isset($opts['timeout'])) + $this->http->set_timeout($opts['timeout']); + if(isset($opts['max_redirects'])) + $this->http->set_max_redirects($opts['max_redirects']); + + // Check if the URL matches a special parser + + if(Formats\Instagram::matches($url)) { + return Formats\Instagram::parse($this->http, $body, $url); + } + + if(Formats\GitHub::matches($url)) { + return Formats\GitHub::parse($body, $url); + } - + + return [ + 'data' => [ + 'type' => 'unknown' + ] + ]; } } diff --git a/tests/InstagramTest.php b/tests/InstagramTest.php index 2c15845..d3addc4 100644 --- a/tests/InstagramTest.php +++ b/tests/InstagramTest.php @@ -71,8 +71,8 @@ class InstagramTest extends PHPUnit_Framework_TestCase { $this->assertEquals(2, count($data['data']['category'])); $this->assertContains('http://tinyletter.com/kmikeym', $data['data']['category']); - $this->assertArrayHasKey('http://tinyletter.com/kmikeym', $data['refs']); - $this->assertEquals(['type'=>'card','name'=>'Mike Merrill','url'=>'http://tinyletter.com/kmikeym','photo'=>'https://instagram.fsjc1-3.fna.fbcdn.net/t51.2885-19/s320x320/12627953_686238411518831_1544976311_a.jpg'], $data['refs']['http://tinyletter.com/kmikeym']); + $this->assertArrayHasKey('http://tinyletter.com/kmikeym', $data['data']['refs']); + $this->assertEquals(['type'=>'card','name'=>'Mike Merrill','url'=>'http://tinyletter.com/kmikeym','photo'=>'https://instagram.fsjc1-3.fna.fbcdn.net/t51.2885-19/s320x320/12627953_686238411518831_1544976311_a.jpg'], $data['data']['refs']['http://tinyletter.com/kmikeym']); } public function testInstagramPhotoWithVenue() { @@ -86,8 +86,8 @@ class InstagramTest extends PHPUnit_Framework_TestCase { $this->assertEquals(1, count($data['data']['location'])); $this->assertContains('https://www.instagram.com/explore/locations/109284789535230/', $data['data']['location']); - $this->assertArrayHasKey('https://www.instagram.com/explore/locations/109284789535230/', $data['refs']); - $venue = $data['refs']['https://www.instagram.com/explore/locations/109284789535230/']; + $this->assertArrayHasKey('https://www.instagram.com/explore/locations/109284789535230/', $data['data']['refs']); + $venue = $data['data']['refs']['https://www.instagram.com/explore/locations/109284789535230/']; $this->assertEquals('XOXO Outpost', $venue['name']); $this->assertEquals('45.5261002', $venue['latitude']); $this->assertEquals('-122.6558081', $venue['longitude']); From f19b8fd7dd59a8045be98bfbb66e4146ceb0e9c2 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sat, 29 Apr 2017 10:45:28 -0700 Subject: [PATCH 15/17] refactor XKCD parsing --- controllers/Parse.php | 18 ------------------ lib/XRay/Parser.php | 4 +++- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/controllers/Parse.php b/controllers/Parse.php index b2cd612..5b33763 100644 --- a/controllers/Parse.php +++ b/controllers/Parse.php @@ -117,24 +117,6 @@ class Parse { - // Check for known services - $host = parse_url($result['url'], PHP_URL_HOST); - - if(in_array($host, ['www.instagram.com','instagram.com'])) { - list($data, $parsed) = Formats\Instagram::parse($result['body'], $result['url'], $this->http); - if($request->get('include_original')) - $data['original'] = $parsed; - $data['url'] = $result['url']; - $data['code'] = $result['code']; - return $this->respond($response, 200, $data); - } - - if($host == 'xkcd.com' && parse_url($url, PHP_URL_PATH) != '/') { - $data = Formats\XKCD::parse($result['body'], $url); - $data['url'] = $result['url']; - $data['code'] = $result['code']; - return $this->respond($response, 200, $data); - } // attempt to parse the page as HTML $doc = new DOMDocument(); diff --git a/lib/XRay/Parser.php b/lib/XRay/Parser.php index 97fab1f..38efb5a 100644 --- a/lib/XRay/Parser.php +++ b/lib/XRay/Parser.php @@ -26,7 +26,9 @@ class Parser { return Formats\GitHub::parse($body, $url); } - + if(Formats\XKCD::matches($url)) { + return Formats\XKCD::parse($body, $url); + } return [ 'data' => [ From 01b53edc9599091f3a9f73f7751cf3816121a460 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sat, 29 Apr 2017 11:15:39 -0700 Subject: [PATCH 16/17] refactor Twitter parser --- README.md | 20 +++++++++ controllers/Parse.php | 80 +----------------------------------- lib/XRay/Fetcher.php | 3 +- lib/XRay/Formats/Twitter.php | 52 +++++++++-------------- lib/XRay/Parser.php | 4 ++ tests/TwitterTest.php | 36 ++++++++-------- 6 files changed, 65 insertions(+), 130 deletions(-) diff --git a/README.md b/README.md index a048ffd..dca7161 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,26 @@ In both cases, the response will be a JSON object containing a key of "type". If You can also make a POST request with the same parameter names. +If you already have an HTML or JSON document you want to parse, you can include that in the parameter `body`. This POST request would look like the below: + +``` +POST /parse +Content-type: application/x-www-form-urlencoded + +url=https://aaronparecki.com/2016/01/16/11/ +&body=.... +``` + +or for Twitter/GitHub where you might have JSON, + +``` +POST /parse +Content-type: application/x-www-form-urlencoded + +url=https://github.com/aaronpk/XRay +&body={"repo":......} +``` + ### Authentication If the URL you are fetching requires authentication, include the access token in the parameter "token", and it will be included in an "Authorization" header when fetching the URL. (It is recommended to use a POST request in this case, to avoid the access token potentially being logged as part of the query string.) This is useful for [Private Webmention](https://indieweb.org/Private-Webmention) verification. diff --git a/controllers/Parse.php b/controllers/Parse.php index 5b33763..44a8b34 100644 --- a/controllers/Parse.php +++ b/controllers/Parse.php @@ -62,12 +62,12 @@ class Parse { } $url = $request->get('url'); - $html = $request->get('html'); + $html = $request->get('html') ?: $request->get('body'); if(!$url && !$html) { return $this->respond($response, 400, [ 'error' => 'missing_url', - 'error_description' => 'Provide a URL or HTML to fetch' + 'error_description' => 'Provide a URL or HTML to fetch', ]); } @@ -236,81 +236,5 @@ class Parse { return $element; } - private function parseTwitterURL(&$request, &$response, $url, $match) { - $fields = ['twitter_api_key','twitter_api_secret','twitter_access_token','twitter_access_token_secret']; - $creds = []; - foreach($fields as $f) { - if($v=$request->get($f)) - $creds[$f] = $v; - } - $data = false; - if(count($creds) == 4) { - list($data, $parsed) = Formats\Twitter::parse($url, $match[1], $creds); - } elseif(count($creds) > 0) { - // If only some Twitter credentials were present, return an error - return $this->respond($response, 400, [ - 'error' => 'missing_parameters', - 'error_description' => 'All 4 Twitter credentials must be included in the request' - ]); - } else { - // Accept Tweet JSON and parse that if provided - $json = $request->get('json'); - if($json) { - list($data, $parsed) = Formats\Twitter::parse($url, $match[1], null, $json); - } - // Skip parsing from the Twitter API if they didn't include credentials - } - - if($data) { - if($request->get('include_original')) - $data['original'] = $parsed; - $data['url'] = $url; - $data['code'] = 200; - return $this->respond($response, 200, $data); - } else { - return $this->respond($response, 200, [ - 'data' => [ - 'type' => 'unknown' - ], - 'url' => $url, - 'code' => 0 - ]); - } - } - - private function parseGitHubURL(&$request, &$response, $url) { - $fields = ['github_access_token']; - $creds = []; - foreach($fields as $f) { - if($v=$request->get($f)) - $creds[$f] = $v; - } - $data = false; - $json = $request->get('json'); - if($json) { - // Accept GitHub JSON and parse that if provided - list($data, $json, $code) = Formats\GitHub::parse($this->http, $url, null, $json); - } else { - // Otherwise fetch the post unauthenticated or with the provided access token - list($data, $json, $code) = Formats\GitHub::parse($this->http, $url, $creds); - } - - if($data) { - if($request->get('include_original')) - $data['original'] = $json; - $data['url'] = $url; - $data['code'] = $code; - return $this->respond($response, 200, $data); - } else { - return $this->respond($response, 200, [ - 'data' => [ - 'type' => 'unknown' - ], - 'url' => $url, - 'code' => $code - ]); - } - } - } diff --git a/lib/XRay/Fetcher.php b/lib/XRay/Fetcher.php index 9b82909..1ead5e4 100644 --- a/lib/XRay/Fetcher.php +++ b/lib/XRay/Fetcher.php @@ -10,7 +10,7 @@ class Fetcher { public function fetch($url, $opts=[]) { if($opts == false) $opts = []; - + if(isset($opts['timeout'])) $this->http->set_timeout($opts['timeout']); if(isset($opts['max_redirects'])) @@ -127,6 +127,7 @@ class Fetcher { } if(count($creds) < 4) { +print_r(debug_backtrace()[1]); return [ 'error_code' => 400, 'error' => 'missing_parameters', diff --git a/lib/XRay/Formats/Twitter.php b/lib/XRay/Formats/Twitter.php index 905f24c..7462dd5 100644 --- a/lib/XRay/Formats/Twitter.php +++ b/lib/XRay/Formats/Twitter.php @@ -39,30 +39,17 @@ class Twitter extends Format { return $tweet; } - public static function parse($url, $tweet_id, $creds, $json=null) { + public static function parse($json, $url) { - $host = parse_url($url, PHP_URL_HOST); - if($host == 'twtr.io') { - $tweet_id = self::b60to10($tweet_id); - } + if(is_string($json)) + $tweet = json_decode($json); + else + $tweet = $json; - if($json) { - if(is_string($json)) - $tweet = json_decode($json); - else - $tweet = $json; - } else { - $twitter = new \Twitter($creds['twitter_api_key'], $creds['twitter_api_secret'], $creds['twitter_access_token'], $creds['twitter_access_token_secret']); - try { - $tweet = $twitter->request('statuses/show/'.$tweet_id, 'GET', ['tweet_mode'=>'extended']); - } catch(\TwitterException $e) { - return [false, false]; - } + if(!$tweet) { + return self::_unknown(); } - if(!$tweet) - return [false, false]; - $entry = array( 'type' => 'entry', 'url' => $url, @@ -89,9 +76,9 @@ class Twitter extends Format { $repostOf = 'https://twitter.com/' . $reposted->user->screen_name . '/status/' . $reposted->id_str; $entry['repost-of'] = $repostOf; - list($repostedEntry) = self::parse($repostOf, $reposted->id_str, null, $reposted); - if(isset($repostedEntry['refs'])) { - foreach($repostedEntry['refs'] as $k=>$v) { + $repostedEntry = self::parse($reposted, $repostOf); + if(isset($repostedEntry['data']['refs'])) { + foreach($repostedEntry['data']['refs'] as $k=>$v) { $refs[$k] = $v; } } @@ -174,28 +161,27 @@ class Twitter extends Format { // Quoted Status if(property_exists($tweet, 'quoted_status')) { $quoteOf = 'https://twitter.com/' . $tweet->quoted_status->user->screen_name . '/status/' . $tweet->quoted_status_id_str; - list($quoted) = self::parse($quoteOf, $tweet->quoted_status_id_str, null, $tweet->quoted_status); - if(isset($quoted['refs'])) { - foreach($quoted['refs'] as $k=>$v) { + $quotedEntry = self::parse($tweet->quoted_status, $quoteOf); + if(isset($quotedEntry['data']['refs'])) { + foreach($quotedEntry['data']['refs'] as $k=>$v) { $refs[$k] = $v; } } - $refs[$quoteOf] = $quoted['data']; + $refs[$quoteOf] = $quotedEntry['data']; } if($author = self::_buildHCardFromTwitterProfile($tweet->user)) { $entry['author'] = $author; } - $response = [ - 'data' => $entry - ]; - if(count($refs)) { - $response['refs'] = $refs; + $entry['refs'] = $refs; } - return [$response, $tweet]; + return [ + 'data' => $entry, + 'original' => $tweet, + ]; } private static function _buildHCardFromTwitterProfile($profile) { diff --git a/lib/XRay/Parser.php b/lib/XRay/Parser.php index 38efb5a..622e12e 100644 --- a/lib/XRay/Parser.php +++ b/lib/XRay/Parser.php @@ -26,6 +26,10 @@ class Parser { return Formats\GitHub::parse($body, $url); } + if(Formats\Twitter::matches($url)) { + return Formats\Twitter::parse($body, $url); + } + if(Formats\XKCD::matches($url)) { return Formats\XKCD::parse($body, $url); } diff --git a/tests/TwitterTest.php b/tests/TwitterTest.php index a766788..f892ad9 100644 --- a/tests/TwitterTest.php +++ b/tests/TwitterTest.php @@ -29,7 +29,7 @@ class TwitterTest extends PHPUnit_Framework_TestCase { public function testBasicProfileInfo() { list($url, $json) = $this->loadTweet('818912506496229376'); - $data = $this->parse(['url' => $url, 'json' => $json]); + $data = $this->parse(['url' => $url, 'body' => $json]); $this->assertEquals('entry', $data['data']['type']); $this->assertEquals('aaronpk dev', $data['data']['author']['name']); @@ -43,7 +43,7 @@ class TwitterTest extends PHPUnit_Framework_TestCase { public function testProfileWithNonExpandedURL() { list($url, $json) = $this->loadTweet('791704641046052864'); - $data = $this->parse(['url' => $url, 'json' => $json]); + $data = $this->parse(['url' => $url, 'body' => $json]); $this->assertEquals('http://agiletortoise.com', $data['data']['author']['url']); } @@ -51,9 +51,9 @@ class TwitterTest extends PHPUnit_Framework_TestCase { public function testBasicTestStuff() { list($url, $json) = $this->loadTweet('818913630569664512'); - $data = $this->parse(['url' => $url, 'json' => $json]); + $data = $this->parse(['url' => $url, 'body' => $json]); - $this->assertEquals(200, $data['code']); + $this->assertEquals(null, $data['code']); // no code is expected if we pass in the body $this->assertEquals('https://twitter.com/pkdev/status/818913630569664512', $data['url']); $this->assertEquals('entry', $data['data']['type']); $this->assertEquals('A tweet with a URL https://indieweb.org/ #and #some #hashtags', $data['data']['content']['text']); @@ -67,14 +67,14 @@ class TwitterTest extends PHPUnit_Framework_TestCase { public function testPositiveTimezone() { list($url, $json) = $this->loadTweet('719914707566649344'); - $data = $this->parse(['url' => $url, 'json' => $json]); + $data = $this->parse(['url' => $url, 'body' => $json]); $this->assertEquals("2016-04-12T16:46:56+01:00", $data['data']['published']); } public function testTweetWithEmoji() { list($url, $json) = $this->loadTweet('818943244553699328'); - $data = $this->parse(['url' => $url, 'json' => $json]); + $data = $this->parse(['url' => $url, 'body' => $json]); $this->assertEquals('entry', $data['data']['type']); $this->assertEquals('Here 🎉 have an emoji', $data['data']['content']['text']); @@ -83,7 +83,7 @@ class TwitterTest extends PHPUnit_Framework_TestCase { public function testHTMLEscaping() { list($url, $json) = $this->loadTweet('818928092383166465'); - $data = $this->parse(['url' => $url, 'json' => $json]); + $data = $this->parse(['url' => $url, 'body' => $json]); $this->assertEquals('entry', $data['data']['type']); $this->assertEquals('Double escaping & & amp', $data['data']['content']['text']); @@ -92,7 +92,7 @@ class TwitterTest extends PHPUnit_Framework_TestCase { public function testTweetWithPhoto() { list($url, $json) = $this->loadTweet('818912506496229376'); - $data = $this->parse(['url' => $url, 'json' => $json]); + $data = $this->parse(['url' => $url, 'body' => $json]); $this->assertEquals('entry', $data['data']['type']); $this->assertEquals('Tweet with a photo and a location', $data['data']['content']['text']); @@ -102,7 +102,7 @@ class TwitterTest extends PHPUnit_Framework_TestCase { public function testTweetWithTwoPhotos() { list($url, $json) = $this->loadTweet('818935308813103104'); - $data = $this->parse(['url' => $url, 'json' => $json]); + $data = $this->parse(['url' => $url, 'body' => $json]); $this->assertEquals('entry', $data['data']['type']); $this->assertEquals('Two photos', $data['data']['content']['text']); @@ -113,7 +113,7 @@ class TwitterTest extends PHPUnit_Framework_TestCase { public function testTweetWithVideo() { list($url, $json) = $this->loadTweet('818913178260160512'); - $data = $this->parse(['url' => $url, 'json' => $json]); + $data = $this->parse(['url' => $url, 'body' => $json]); $this->assertEquals('entry', $data['data']['type']); $this->assertEquals('Tweet with a video', $data['data']['content']['text']); @@ -123,12 +123,12 @@ class TwitterTest extends PHPUnit_Framework_TestCase { public function testTweetWithLocation() { list($url, $json) = $this->loadTweet('818912506496229376'); - $data = $this->parse(['url' => $url, 'json' => $json]); + $data = $this->parse(['url' => $url, 'body' => $json]); $this->assertEquals('entry', $data['data']['type']); $this->assertEquals('Tweet with a photo and a location', $data['data']['content']['text']); $this->assertEquals('https://api.twitter.com/1.1/geo/id/ac88a4f17a51c7fc.json', $data['data']['location']); - $location = $data['refs']['https://api.twitter.com/1.1/geo/id/ac88a4f17a51c7fc.json']; + $location = $data['data']['refs']['https://api.twitter.com/1.1/geo/id/ac88a4f17a51c7fc.json']; $this->assertEquals('adr', $location['type']); $this->assertEquals('Portland', $location['locality']); $this->assertEquals('United States', $location['country-name']); @@ -138,38 +138,38 @@ class TwitterTest extends PHPUnit_Framework_TestCase { public function testRetweet() { list($url, $json) = $this->loadTweet('818913351623245824'); - $data = $this->parse(['url' => $url, 'json' => $json]); + $data = $this->parse(['url' => $url, 'body' => $json]); $this->assertEquals('entry', $data['data']['type']); $this->assertArrayNotHasKey('content', $data['data']); $repostOf = 'https://twitter.com/aaronpk/status/817414679131660288'; $this->assertEquals($repostOf, $data['data']['repost-of']); - $tweet = $data['refs'][$repostOf]; + $tweet = $data['data']['refs'][$repostOf]; $this->assertEquals('Yeah that\'s me http://xkcd.com/1782/', $tweet['content']['text']); } public function testRetweetWithPhoto() { list($url, $json) = $this->loadTweet('820039442773798912'); - $data = $this->parse(['url' => $url, 'json' => $json]); + $data = $this->parse(['url' => $url, 'body' => $json]); $this->assertEquals('entry', $data['data']['type']); $this->assertArrayNotHasKey('content', $data['data']); $this->assertArrayNotHasKey('photo', $data['data']); $repostOf = 'https://twitter.com/phlaimeaux/status/819943954724556800'; $this->assertEquals($repostOf, $data['data']['repost-of']); - $tweet = $data['refs'][$repostOf]; + $tweet = $data['data']['refs'][$repostOf]; $this->assertEquals('this headline is such a rollercoaster', $tweet['content']['text']); } public function testQuotedTweet() { list($url, $json) = $this->loadTweet('818913488609251331'); - $data = $this->parse(['url' => $url, 'json' => $json]); + $data = $this->parse(['url' => $url, 'body' => $json]); $this->assertEquals('entry', $data['data']['type']); $this->assertEquals('Quoted tweet with a #hashtag https://twitter.com/aaronpk/status/817414679131660288', $data['data']['content']['text']); - $tweet = $data['refs']['https://twitter.com/aaronpk/status/817414679131660288']; + $tweet = $data['data']['refs']['https://twitter.com/aaronpk/status/817414679131660288']; $this->assertEquals('Yeah that\'s me http://xkcd.com/1782/', $tweet['content']['text']); } From 78e3e16592b75637c9ed9c3f8fe0f7b99e8638c2 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sat, 29 Apr 2017 12:33:02 -0700 Subject: [PATCH 17/17] finishes the refactor! --- README.md | 55 +++++++++++++- composer.lock | 10 +-- controllers/Parse.php | 151 +++++--------------------------------- lib/XRay/Fetcher.php | 9 ++- lib/XRay/Formats/HTML.php | 132 +++++++++++++++++++++++++++++++++ lib/XRay/Formats/Mf2.php | 45 ++++++------ lib/XRay/Parser.php | 7 +- tests/ParseTest.php | 51 ++++++------- 8 files changed, 267 insertions(+), 193 deletions(-) create mode 100644 lib/XRay/Formats/HTML.php diff --git a/README.md b/README.md index dca7161..d688513 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,53 @@ The contents of the URL is checked in the following order: * h-recipe * h-product -## Parse API +## Library + +XRay can be used as a library in your PHP project. The easiest way to install it and its dependencies is via composer. + +``` +composer require p3k/xray +``` + +Basic usage: + +```php +$xray = new p3k\XRay(); +$parsed = $xray->parse('https://aaronparecki.com/2017/04/28/9/'); +``` + +If you already have an HTML or JSON document you want to parse, you can pass it as a string in the second parameter. + +```php +$xray = new p3k\XRay(); +$html = '....'; +$parsed = $xray->parse('https://aaronparecki.com/2017/04/28/9/', $html); +``` + +In both cases, you can add an additional parameter to configure various options of how XRay will behave. Below is a list of the options. + +* `timeout` - The timeout in seconds to wait for any HTTP requests +* `max_redirects` - The maximum number of redirects to follow +* `include_original` - Will also return the full document fetched +* `target` - Specify a target URL, and XRay will first check if that URL is on the page, and only if it is, will continue to parse the page. This is useful when you're using XRay to verify an incoming webmention. + +Additionally, the following parameters are supported when making requests that use the Twitter or GitHub API. See the authentication section below for details. + +```php +$xray = new p3k\XRay(); + +$parsed = $xray->parse('https://aaronparecki.com/2017/04/28/9/', [ + 'timeout' => 30 +]); + +$parsed = $xray->parse('https://aaronparecki.com/2017/04/28/9/', $html, [ + 'target' => 'http://example.com/' +]); +``` + +## API + +XRay can also be used as an API to provide its parsing capabilities over an HTTP service. To parse a page and return structured data for the contents of the page, simply pass a url to the parse route. @@ -84,6 +130,13 @@ You should only send Twitter credentials when the URL you are trying to parse is * twitter_access_token_secret - Your Twitter secret access token +### GitHub Authentication + +XRay uses the GitHub API to fetch GitHub URLs, which provides higher rate limits when used with authentication. You can pass a GitHub access token along with the request and XRay will use it when making requests to the API. + +* github_access_token - A GitHub access token + + ### Error Response ```json diff --git a/composer.lock b/composer.lock index 645eae6..10303b9 100644 --- a/composer.lock +++ b/composer.lock @@ -256,16 +256,16 @@ }, { "name": "p3k/http", - "version": "0.1.4", + "version": "0.1.5", "source": { "type": "git", "url": "https://github.com/aaronpk/p3k-http.git", - "reference": "136aac6f7ecd6d6e16e8ff9286b43110680c49ab" + "reference": "3740fe135e6d58457d7528e7c05a67b68e020a79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aaronpk/p3k-http/zipball/136aac6f7ecd6d6e16e8ff9286b43110680c49ab", - "reference": "136aac6f7ecd6d6e16e8ff9286b43110680c49ab", + "url": "https://api.github.com/repos/aaronpk/p3k-http/zipball/3740fe135e6d58457d7528e7c05a67b68e020a79", + "reference": "3740fe135e6d58457d7528e7c05a67b68e020a79", "shasum": "" }, "require": { @@ -290,7 +290,7 @@ ], "description": "A simple wrapper API around the PHP curl functions", "homepage": "https://github.com/aaronpk/p3k-http", - "time": "2017-04-28T19:46:12+00:00" + "time": "2017-04-29T17:43:29+00:00" }, { "name": "p3k/timezone", diff --git a/controllers/Parse.php b/controllers/Parse.php index 44a8b34..dbc05e4 100644 --- a/controllers/Parse.php +++ b/controllers/Parse.php @@ -41,10 +41,6 @@ class Parse { return $response; } - private static function toHtmlEntities($input) { - return mb_convert_encoding($input, 'HTML-ENTITIES', mb_detect_encoding($input)); - } - public function parse(Request $request, Response $response) { $opts = []; @@ -57,6 +53,10 @@ class Parse { $opts['max_redirects'] = (int)$request->get('max_redirects'); } + if($request->get('target')) { + $opts['target'] = $request->get('target'); + } + if($request->get('pretty')) { $this->_pretty = true; } @@ -105,136 +105,23 @@ class Parse { if(isset($parsed['code'])) $result['code'] = $parsed['code']; - $data = [ - 'data' => $parsed['data'], - 'url' => $result['url'], - 'code' => $result['code'] - ]; - if($request->get('include_original') && isset($parsed['original'])) - $data['original'] = $parsed['original']; - - return $this->respond($response, 200, $data); - - - - - // attempt to parse the page as HTML - $doc = new DOMDocument(); - @$doc->loadHTML(self::toHtmlEntities($result['body'])); - - if(!$doc) { - return $this->respond($response, 200, [ - 'error' => 'invalid_content', - 'error_description' => 'The document could not be parsed as HTML' - ]); - } - - $xpath = new DOMXPath($doc); - - // Check for meta http equiv and replace the status code if present - foreach($xpath->query('//meta[translate(@http-equiv,\'STATUS\',\'status\')=\'status\']') as $el) { - $equivStatus = ''.$el->getAttribute('content'); - if($equivStatus && is_string($equivStatus)) { - if(preg_match('/^(\d+)/', $equivStatus, $match)) { - $result['code'] = (int)$match[1]; - } - } - } - - // If a target parameter was provided, make sure a link to it exists on the page - if($target=$request->get('target')) { - $found = []; - if($target) { - self::xPathFindNodeWithAttribute($xpath, 'a', 'href', function($u) use($target, &$found){ - if($u == $target) { - $found[$u] = null; - } - }); - self::xPathFindNodeWithAttribute($xpath, 'img', 'src', function($u) use($target, &$found){ - if($u == $target) { - $found[$u] = null; - } - }); - self::xPathFindNodeWithAttribute($xpath, 'video', 'src', function($u) use($target, &$found){ - if($u == $target) { - $found[$u] = null; - } - }); - self::xPathFindNodeWithAttribute($xpath, 'audio', 'src', function($u) use($target, &$found){ - if($u == $target) { - $found[$u] = null; - } - }); - } - - if(!$found) { - return $this->respond($response, 200, [ - 'error' => 'no_link_found', - 'error_description' => 'The source document does not have a link to the target URL', - 'url' => $result['url'], - 'code' => $result['code'], - ]); - } - } - - // If the URL has a fragment ID, find the DOM starting at that node and parse it instead - $html = $result['body']; - - $fragment = parse_url($url, PHP_URL_FRAGMENT); - if($fragment) { - $fragElement = self::xPathGetElementById($xpath, $fragment); - if($fragElement) { - $html = $doc->saveHTML($fragElement); - $foundFragment = true; - } else { - $foundFragment = false; - } - } - - // Now start pulling in the data from the page. Start by looking for microformats2 - $mf2 = mf2\Parse($html, $result['url']); - - if($mf2 && count($mf2['items']) > 0) { - $data = Formats\Mf2::parse($mf2, $result['url'], $this->http); - if($data) { - if($fragment) { - $data['info'] = [ - 'found_fragment' => $foundFragment - ]; - } - if($request->get('include_original')) - $data['original'] = $html; - $data['url'] = $result['url']; // this will be the effective URL after following redirects - $data['code'] = $result['code']; - return $this->respond($response, 200, $data); - } - } - - // TODO: look for other content like OEmbed or other known services later - - return $this->respond($response, 200, [ - 'data' => [ - 'type' => 'unknown', - ], - 'url' => $result['url'], - 'code' => $result['code'] - ]); - } - - private static function xPathFindNodeWithAttribute($xpath, $node, $attr, $callback) { - foreach($xpath->query('//'.$node.'[@'.$attr.']') as $el) { - $v = $el->getAttribute($attr); - $callback($v); - } - } + if(!empty($parsed['error'])) { + $error_code = isset($parsed['error_code']) ? $parsed['error_code'] : 200; + unset($parsed['error_code']); + return $this->respond($response, $error_code, $parsed); + } else { + $data = [ + 'data' => $parsed['data'], + 'url' => $result['url'], + 'code' => $result['code'] + ]; + if(isset($parsed['info'])) + $data['info'] = $parsed['info']; + if($request->get('include_original') && isset($parsed['original'])) + $data['original'] = $parsed['original']; - private static function xPathGetElementById($xpath, $id) { - $element = null; - foreach($xpath->query("//*[@id='$id']") as $el) { - $element = $el; + return $this->respond($response, 200, $data); } - return $element; } - } diff --git a/lib/XRay/Fetcher.php b/lib/XRay/Fetcher.php index 1ead5e4..8139cf8 100644 --- a/lib/XRay/Fetcher.php +++ b/lib/XRay/Fetcher.php @@ -78,7 +78,8 @@ class Fetcher { if($result['code'] == 410) { // 410 Gone responses are valid and should not return an error return $this->respond($response, 200, [ - 'TODO' => [ + 'data' => [ + 'type' => 'unknown' ], 'url' => $result['url'], 'code' => $result['code'] @@ -111,6 +112,11 @@ class Fetcher { ]; } + // If the original URL had a fragment, include it in the final URL + if(($fragment=parse_url($url, PHP_URL_FRAGMENT)) && !parse_url($result['url'], PHP_URL_FRAGMENT)) { + $result['url'] .= '#'.$fragment; + } + return [ 'url' => $result['url'], 'body' => $result['body'], @@ -127,7 +133,6 @@ class Fetcher { } if(count($creds) < 4) { -print_r(debug_backtrace()[1]); return [ 'error_code' => 400, 'error' => 'missing_parameters', diff --git a/lib/XRay/Formats/HTML.php b/lib/XRay/Formats/HTML.php new file mode 100644 index 0000000..45c6c45 --- /dev/null +++ b/lib/XRay/Formats/HTML.php @@ -0,0 +1,132 @@ + [ + 'type' => 'unknown', + ], + 'url' => $url, + ]; + + // attempt to parse the page as HTML + $doc = new DOMDocument(); + @$doc->loadHTML(self::toHtmlEntities($html)); + + if(!$doc) { + return [ + 'error' => 'invalid_content', + 'error_description' => 'The document could not be parsed as HTML' + ]; + } + + $xpath = new DOMXPath($doc); + + // Check for meta http equiv and replace the status code if present + foreach($xpath->query('//meta[translate(@http-equiv,\'STATUS\',\'status\')=\'status\']') as $el) { + $equivStatus = ''.$el->getAttribute('content'); + if($equivStatus && is_string($equivStatus)) { + if(preg_match('/^(\d+)/', $equivStatus, $match)) { + $result['code'] = (int)$match[1]; + } + } + } + + // If a target parameter was provided, make sure a link to it exists on the page + if(isset($opts['target'])) { + $target = $opts['target']; + + $found = []; + if($target) { + self::xPathFindNodeWithAttribute($xpath, 'a', 'href', function($u) use($target, &$found){ + if($u == $target) { + $found[$u] = null; + } + }); + self::xPathFindNodeWithAttribute($xpath, 'img', 'src', function($u) use($target, &$found){ + if($u == $target) { + $found[$u] = null; + } + }); + self::xPathFindNodeWithAttribute($xpath, 'video', 'src', function($u) use($target, &$found){ + if($u == $target) { + $found[$u] = null; + } + }); + self::xPathFindNodeWithAttribute($xpath, 'audio', 'src', function($u) use($target, &$found){ + if($u == $target) { + $found[$u] = null; + } + }); + } + + if(!$found) { + return [ + 'error' => 'no_link_found', + 'error_description' => 'The source document does not have a link to the target URL', + 'code' => isset($result['code']) ? $result['code'] : 200, + 'url' => $url + ]; + } + } + + // If the URL has a fragment ID, find the DOM starting at that node and parse it instead + $fragment = parse_url($url, PHP_URL_FRAGMENT); + if($fragment) { + $fragElement = self::xPathGetElementById($xpath, $fragment); + if($fragElement) { + $html = $doc->saveHTML($fragElement); + $foundFragment = true; + } else { + $foundFragment = false; + } + } + + // Now start pulling in the data from the page. Start by looking for microformats2 + $mf2 = \mf2\Parse($html, $url); + + if($mf2 && count($mf2['items']) > 0) { + $data = Formats\Mf2::parse($mf2, $url, $http); + $result = array_merge($result, $data); + if($data) { + if($fragment) { + $result['info'] = [ + 'found_fragment' => $foundFragment + ]; + } + $result['original'] = $html; + $result['url'] = $url; // this will be the effective URL after following redirects + } + } + return $result; + } + + private static function toHtmlEntities($input) { + return mb_convert_encoding($input, 'HTML-ENTITIES', mb_detect_encoding($input)); + } + + private static function xPathFindNodeWithAttribute($xpath, $node, $attr, $callback) { + foreach($xpath->query('//'.$node.'[@'.$attr.']') as $el) { + $v = $el->getAttribute($attr); + $callback($v); + } + } + + private static function xPathGetElementById($xpath, $id) { + $element = null; + foreach($xpath->query("//*[@id='$id']") as $el) { + $element = $el; + } + return $element; + } + +} diff --git a/lib/XRay/Formats/Mf2.php b/lib/XRay/Formats/Mf2.php index 93f1890..9bf6605 100644 --- a/lib/XRay/Formats/Mf2.php +++ b/lib/XRay/Formats/Mf2.php @@ -2,7 +2,6 @@ namespace p3k\XRay\Formats; use HTMLPurifier, HTMLPurifier_Config; -use Parse; class Mf2 { @@ -14,31 +13,31 @@ class Mf2 { if(count($mf2['items']) == 1) { $item = $mf2['items'][0]; if(in_array('h-entry', $item['type']) || in_array('h-cite', $item['type'])) { - Parse::debug("mf2:0: Recognized $url as an h-entry it is the only item on the page"); + #Parse::debug("mf2:0: Recognized $url as an h-entry it is the only item on the page"); return self::parseAsHEntry($mf2, $item, $http); } if(in_array('h-event', $item['type'])) { - Parse::debug("mf2:0: Recognized $url as an h-event it is the only item on the page"); + #Parse::debug("mf2:0: Recognized $url as an h-event it is the only item on the page"); return self::parseAsHEvent($mf2, $item, $http); } if(in_array('h-review', $item['type'])) { - Parse::debug("mf2:0: Recognized $url as an h-review it is the only item on the page"); + #Parse::debug("mf2:0: Recognized $url as an h-review it is the only item on the page"); return self::parseAsHReview($mf2, $item, $http); } if(in_array('h-recipe', $item['type'])) { - Parse::debug("mf2:0: Recognized $url as an h-recipe it is the only item on the page"); + #Parse::debug("mf2:0: Recognized $url as an h-recipe it is the only item on the page"); return self::parseAsHRecipe($mf2, $item, $http); } if(in_array('h-product', $item['type'])) { - Parse::debug("mf2:0: Recognized $url as an h-product it is the only item on the page"); + #Parse::debug("mf2:0: Recognized $url as an h-product it is the only item on the page"); return self::parseAsHProduct($mf2, $item, $http); } if(in_array('h-feed', $item['type'])) { - Parse::debug("mf2:0: Recognized $url as an h-feed because it is the only item on the page"); + #Parse::debug("mf2:0: Recognized $url as an h-feed because it is the only item on the page"); return self::parseAsHFeed($mf2, $http); } if(in_array('h-card', $item['type'])) { - Parse::debug("mf2:0: Recognized $url as an h-card it is the only item on the page"); + #Parse::debug("mf2:0: Recognized $url as an h-card it is the only item on the page"); return self::parseAsHCard($item, $http, $url); } } @@ -50,7 +49,7 @@ class Mf2 { $urls = $item['properties']['url']; $urls = array_map('\p3k\XRay\normalize_url', $urls); if(in_array($url, $urls)) { - Parse::debug("mf2:1: Recognized $url as a permalink because an object on the page matched the URL of the request"); + #Parse::debug("mf2:1: Recognized $url as a permalink because an object on the page matched the URL of the request"); if(in_array('h-card', $item['type'])) { return self::parseAsHCard($item, $http, $url); } elseif(in_array('h-entry', $item['type']) || in_array('h-cite', $item['type'])) { @@ -64,7 +63,7 @@ class Mf2 { } elseif(in_array('h-product', $item['type'])) { return self::parseAsHProduct($mf2, $item, $http); } else { - Parse::debug('This object was not a recognized type.'); + #Parse::debug('This object was not a recognized type.'); return false; } } @@ -106,7 +105,7 @@ class Mf2 { if(count(array_filter($mf2['items'], function($item){ return in_array('h-entry', $item['type']); })) > 1) { - Parse::debug("mf2:2: Recognized $url as an h-feed because there are more than one object on the page"); + #Parse::debug("mf2:2: Recognized $url as an h-feed because there are more than one object on the page"); return self::parseAsHFeed($mf2, $http); } } @@ -114,7 +113,7 @@ class Mf2 { // If the first item is an h-feed, parse as a feed $first = $mf2['items'][0]; if(in_array('h-feed', $first['type'])) { - Parse::debug("mf2:3: Recognized $url as an h-feed because the first item is an h-feed"); + #Parse::debug("mf2:3: Recognized $url as an h-feed because the first item is an h-feed"); return self::parseAsHFeed($mf2, $http); } @@ -122,24 +121,24 @@ class Mf2 { foreach($mf2['items'] as $item) { // Otherwise check for a recognized h-entr* object if(in_array('h-entry', $item['type']) || in_array('h-cite', $item['type'])) { - Parse::debug("mf2:6: $url is falling back to the first h-entry on the page"); + #Parse::debug("mf2:6: $url is falling back to the first h-entry on the page"); return self::parseAsHEntry($mf2, $item, $http); } elseif(in_array('h-event', $item['type'])) { - Parse::debug("mf2:6: $url is falling back to the first h-event on the page"); + #Parse::debug("mf2:6: $url is falling back to the first h-event on the page"); return self::parseAsHEvent($mf2, $item, $http); } elseif(in_array('h-review', $item['type'])) { - Parse::debug("mf2:6: $url is falling back to the first h-review on the page"); + #Parse::debug("mf2:6: $url is falling back to the first h-review on the page"); return self::parseAsHReview($mf2, $item, $http); } elseif(in_array('h-recipe', $item['type'])) { - Parse::debug("mf2:6: $url is falling back to the first h-recipe on the page"); + #Parse::debug("mf2:6: $url is falling back to the first h-recipe on the page"); return self::parseAsHReview($mf2, $item, $http); } elseif(in_array('h-product', $item['type'])) { - Parse::debug("mf2:6: $url is falling back to the first h-product on the page"); + #Parse::debug("mf2:6: $url is falling back to the first h-product on the page"); return self::parseAsHProduct($mf2, $item, $http); } } - Parse::debug("mf2:E: No object at $url was recognized"); + #Parse::debug("mf2:E: No object at $url was recognized"); return false; } @@ -311,7 +310,7 @@ class Mf2 { ]; if(count($refs)) { - $response['refs'] = $refs; + $response['data']['refs'] = $refs; } return $response; @@ -345,7 +344,7 @@ class Mf2 { ]; if(count($refs)) { - $response['refs'] = $refs; + $response['data']['refs'] = $refs; } return $response; @@ -376,7 +375,7 @@ class Mf2 { ]; if(count($refs)) { - $response['refs'] = $refs; + $response['data']['refs'] = $refs; } return $response; @@ -403,7 +402,7 @@ class Mf2 { ]; if(count($refs)) { - $response['refs'] = $refs; + $response['data']['refs'] = $refs; } return $response; @@ -457,7 +456,7 @@ class Mf2 { ]; if(count($refs)) { - $response['refs'] = $refs; + $response['data']['refs'] = $refs; } return $response; diff --git a/lib/XRay/Parser.php b/lib/XRay/Parser.php index 622e12e..639aba7 100644 --- a/lib/XRay/Parser.php +++ b/lib/XRay/Parser.php @@ -34,11 +34,8 @@ class Parser { return Formats\XKCD::parse($body, $url); } - return [ - 'data' => [ - 'type' => 'unknown' - ] - ]; + // No special parsers matched, parse for Microformats now + return Formats\HTML::parse($this->http, $body, $url, $opts); } } diff --git a/tests/ParseTest.php b/tests/ParseTest.php index d4e45eb..56e3724 100644 --- a/tests/ParseTest.php +++ b/tests/ParseTest.php @@ -205,9 +205,9 @@ class ParseTest extends PHPUnit_Framework_TestCase { $data = json_decode($body, true); $this->assertEquals('entry', $data['data']['type']); $this->assertEquals('http://example.com/100', $data['data']['in-reply-to'][0]); - $this->assertArrayHasKey('http://example.com/100', $data['refs']); - $this->assertEquals('Example Post', $data['refs']['http://example.com/100']['name']); - $this->assertEquals('http://example.com/100', $data['refs']['http://example.com/100']['url']); + $this->assertArrayHasKey('http://example.com/100', $data['data']['refs']); + $this->assertEquals('Example Post', $data['data']['refs']['http://example.com/100']['name']); + $this->assertEquals('http://example.com/100', $data['data']['refs']['http://example.com/100']['url']); } public function testPersonTagIsURL() { @@ -230,10 +230,10 @@ class ParseTest extends PHPUnit_Framework_TestCase { $data = json_decode($body, true); $this->assertEquals('entry', $data['data']['type']); $this->assertEquals('http://alice.example.com/', $data['data']['category'][0]); - $this->assertArrayHasKey('http://alice.example.com/', $data['refs']); - $this->assertEquals('card', $data['refs']['http://alice.example.com/']['type']); - $this->assertEquals('http://alice.example.com/', $data['refs']['http://alice.example.com/']['url']); - $this->assertEquals('Alice', $data['refs']['http://alice.example.com/']['name']); + $this->assertArrayHasKey('http://alice.example.com/', $data['data']['refs']); + $this->assertEquals('card', $data['data']['refs']['http://alice.example.com/']['type']); + $this->assertEquals('http://alice.example.com/', $data['data']['refs']['http://alice.example.com/']['url']); + $this->assertEquals('Alice', $data['data']['refs']['http://alice.example.com/']['name']); } public function testSyndicationIsURL() { @@ -372,10 +372,10 @@ class ParseTest extends PHPUnit_Framework_TestCase { $this->assertEquals($url, $data['data']['url']); $this->assertEquals('2016-02-09T18:30', $data['data']['start']); $this->assertEquals('2016-02-09T19:30', $data['data']['end']); - $this->assertArrayHasKey('http://source.example.com/venue', $data['refs']); - $this->assertEquals('card', $data['refs']['http://source.example.com/venue']['type']); - $this->assertEquals('http://source.example.com/venue', $data['refs']['http://source.example.com/venue']['url']); - $this->assertEquals('Venue', $data['refs']['http://source.example.com/venue']['name']); + $this->assertArrayHasKey('http://source.example.com/venue', $data['data']['refs']); + $this->assertEquals('card', $data['data']['refs']['http://source.example.com/venue']['type']); + $this->assertEquals('http://source.example.com/venue', $data['data']['refs']['http://source.example.com/venue']['url']); + $this->assertEquals('Venue', $data['data']['refs']['http://source.example.com/venue']['name']); } public function testMf2ReviewOfProduct() { @@ -395,10 +395,10 @@ class ParseTest extends PHPUnit_Framework_TestCase { $this->assertContains('red', $data['data']['category']); $this->assertContains('blue', $data['data']['category']); $this->assertContains('http://product.example.com/', $data['data']['item']); - $this->assertArrayHasKey('http://product.example.com/', $data['refs']); - $this->assertEquals('product', $data['refs']['http://product.example.com/']['type']); - $this->assertEquals('The Reviewed Product', $data['refs']['http://product.example.com/']['name']); - $this->assertEquals('http://product.example.com/', $data['refs']['http://product.example.com/']['url']); + $this->assertArrayHasKey('http://product.example.com/', $data['data']['refs']); + $this->assertEquals('product', $data['data']['refs']['http://product.example.com/']['type']); + $this->assertEquals('The Reviewed Product', $data['data']['refs']['http://product.example.com/']['name']); + $this->assertEquals('http://product.example.com/', $data['data']['refs']['http://product.example.com/']['url']); } public function testMf2ReviewOfHCard() { @@ -416,10 +416,10 @@ class ParseTest extends PHPUnit_Framework_TestCase { $this->assertEquals('5', $data['data']['best']); $this->assertEquals('This is the full text of the review', $data['data']['content']['text']); $this->assertContains('http://business.example.com/', $data['data']['item']); - $this->assertArrayHasKey('http://business.example.com/', $data['refs']); - $this->assertEquals('card', $data['refs']['http://business.example.com/']['type']); - $this->assertEquals('The Reviewed Business', $data['refs']['http://business.example.com/']['name']); - $this->assertEquals('http://business.example.com/', $data['refs']['http://business.example.com/']['url']); + $this->assertArrayHasKey('http://business.example.com/', $data['data']['refs']); + $this->assertEquals('card', $data['data']['refs']['http://business.example.com/']['type']); + $this->assertEquals('The Reviewed Business', $data['data']['refs']['http://business.example.com/']['name']); + $this->assertEquals('http://business.example.com/', $data['data']['refs']['http://business.example.com/']['url']); } public function testMf1Review() { @@ -438,10 +438,10 @@ class ParseTest extends PHPUnit_Framework_TestCase { $this->assertEquals('5', $data['data']['best']); $this->assertEquals('This is the full text of the review', $data['data']['content']['text']); // $this->assertContains('http://product.example.com/', $data['data']['item']); - // $this->assertArrayHasKey('http://product.example.com/', $data['refs']); - // $this->assertEquals('product', $data['refs']['http://product.example.com/']['type']); - // $this->assertEquals('The Reviewed Product', $data['refs']['http://product.example.com/']['name']); - // $this->assertEquals('http://product.example.com/', $data['refs']['http://product.example.com/']['url']); + // $this->assertArrayHasKey('http://product.example.com/', $data['data']['refs']); + // $this->assertEquals('product', $data['data']['refs']['http://product.example.com/']['type']); + // $this->assertEquals('The Reviewed Product', $data['data']['refs']['http://product.example.com/']['name']); + // $this->assertEquals('http://product.example.com/', $data['data']['refs']['http://product.example.com/']['url']); } @@ -473,8 +473,8 @@ class ParseTest extends PHPUnit_Framework_TestCase { $this->assertEquals('entry', $data['data']['type']); $this->assertEquals('https://www.facebook.com/555707837940351#tantek', $data['data']['url']); $this->assertContains('https://www.facebook.com/tantek.celik', $data['data']['invitee']); - $this->assertArrayHasKey('https://www.facebook.com/tantek.celik', $data['refs']); - $this->assertEquals('Tantek Çelik', $data['refs']['https://www.facebook.com/tantek.celik']['name']); + $this->assertArrayHasKey('https://www.facebook.com/tantek.celik', $data['data']['refs']); + $this->assertEquals('Tantek Çelik', $data['data']['refs']['https://www.facebook.com/tantek.celik']['name']); } public function testEntryAtFragmentID() { @@ -485,6 +485,7 @@ class ParseTest extends PHPUnit_Framework_TestCase { $this->assertEquals(200, $response->getStatusCode()); $data = json_decode($body, true); $this->assertEquals('entry', $data['data']['type']); + $this->assertEquals('Comment text', $data['data']['content']['text']); $this->assertEquals('http://source.example.com/fragment-id#comment-1000', $data['data']['url']); $this->assertTrue($data['info']['found_fragment']); }