From 60b094d5fa5ad9a58397c90b827fd8e1e99c8788 Mon Sep 17 00:00:00 2001 From: "Marcel Kapfer (mmk2410)" Date: Sat, 7 May 2016 12:59:40 +0200 Subject: [PATCH] add composer's vendor directory --- .gitignore | 1 - vendor/autoload.php | 7 + vendor/bin/picofeed | 1 + vendor/bshaffer/oauth2-server-php/.gitignore | 5 + vendor/bshaffer/oauth2-server-php/.travis.yml | 25 + .../bshaffer/oauth2-server-php/CHANGELOG.md | 165 ++ vendor/bshaffer/oauth2-server-php/LICENSE | 21 + vendor/bshaffer/oauth2-server-php/README.md | 8 + .../bshaffer/oauth2-server-php/composer.json | 27 + vendor/bshaffer/oauth2-server-php/phpunit.xml | 25 + .../src/OAuth2/Autoloader.php | 48 + .../ClientAssertionTypeInterface.php | 15 + .../OAuth2/ClientAssertionType/HttpBasic.php | 123 ++ .../OAuth2/Controller/AuthorizeController.php | 383 ++++ .../AuthorizeControllerInterface.php | 43 + .../OAuth2/Controller/ResourceController.php | 111 ++ .../ResourceControllerInterface.php | 26 + .../src/OAuth2/Controller/TokenController.php | 274 +++ .../Controller/TokenControllerInterface.php | 32 + .../OAuth2/Encryption/EncryptionInterface.php | 11 + .../src/OAuth2/Encryption/FirebaseJwt.php | 47 + .../src/OAuth2/Encryption/Jwt.php | 173 ++ .../OAuth2/GrantType/AuthorizationCode.php | 100 + .../OAuth2/GrantType/ClientCredentials.php | 67 + .../OAuth2/GrantType/GrantTypeInterface.php | 20 + .../src/OAuth2/GrantType/JwtBearer.php | 226 +++ .../src/OAuth2/GrantType/RefreshToken.php | 111 ++ .../src/OAuth2/GrantType/UserCredentials.php | 83 + .../OpenID/Controller/AuthorizeController.php | 106 + .../AuthorizeControllerInterface.php | 10 + .../OpenID/Controller/UserInfoController.php | 58 + .../UserInfoControllerInterface.php | 23 + .../OpenID/GrantType/AuthorizationCode.php | 33 + .../OpenID/ResponseType/AuthorizationCode.php | 60 + .../AuthorizationCodeInterface.php | 27 + .../OpenID/ResponseType/CodeIdToken.php | 24 + .../ResponseType/CodeIdTokenInterface.php | 9 + .../OAuth2/OpenID/ResponseType/IdToken.php | 124 ++ .../OpenID/ResponseType/IdTokenInterface.php | 29 + .../OpenID/ResponseType/IdTokenToken.php | 27 + .../ResponseType/IdTokenTokenInterface.php | 9 + .../Storage/AuthorizationCodeInterface.php | 37 + .../OpenID/Storage/UserClaimsInterface.php | 38 + .../oauth2-server-php/src/OAuth2/Request.php | 213 +++ .../src/OAuth2/RequestInterface.php | 16 + .../oauth2-server-php/src/OAuth2/Response.php | 369 ++++ .../src/OAuth2/ResponseInterface.php | 24 + .../src/OAuth2/ResponseType/AccessToken.php | 194 ++ .../ResponseType/AccessTokenInterface.php | 34 + .../OAuth2/ResponseType/AuthorizationCode.php | 100 + .../AuthorizationCodeInterface.php | 30 + .../OAuth2/ResponseType/JwtAccessToken.php | 124 ++ .../ResponseType/ResponseTypeInterface.php | 8 + .../oauth2-server-php/src/OAuth2/Scope.php | 103 + .../src/OAuth2/ScopeInterface.php | 40 + .../oauth2-server-php/src/OAuth2/Server.php | 832 ++++++++ .../OAuth2/Storage/AccessTokenInterface.php | 63 + .../Storage/AuthorizationCodeInterface.php | 86 + .../src/OAuth2/Storage/Cassandra.php | 469 +++++ .../Storage/ClientCredentialsInterface.php | 49 + .../src/OAuth2/Storage/ClientInterface.php | 66 + .../src/OAuth2/Storage/CouchbaseDB.php | 331 ++++ .../src/OAuth2/Storage/DynamoDB.php | 528 +++++ .../src/OAuth2/Storage/JwtAccessToken.php | 88 + .../Storage/JwtAccessTokenInterface.php | 14 + .../src/OAuth2/Storage/JwtBearerInterface.php | 74 + .../src/OAuth2/Storage/Memory.php | 369 ++++ .../src/OAuth2/Storage/Mongo.php | 333 ++++ .../src/OAuth2/Storage/Pdo.php | 543 ++++++ .../src/OAuth2/Storage/PublicKeyInterface.php | 16 + .../src/OAuth2/Storage/Redis.php | 317 +++ .../OAuth2/Storage/RefreshTokenInterface.php | 82 + .../src/OAuth2/Storage/ScopeInterface.php | 46 + .../Storage/UserCredentialsInterface.php | 52 + .../src/OAuth2/TokenType/Bearer.php | 130 ++ .../src/OAuth2/TokenType/Mac.php | 22 + .../OAuth2/TokenType/TokenTypeInterface.php | 21 + .../test/OAuth2/AutoloadTest.php | 16 + .../Controller/AuthorizeControllerTest.php | 476 +++++ .../Controller/ResourceControllerTest.php | 175 ++ .../OAuth2/Controller/TokenControllerTest.php | 289 +++ .../OAuth2/Encryption/FirebaseJwtTest.php | 102 + .../test/OAuth2/Encryption/JwtTest.php | 102 + .../GrantType/AuthorizationCodeTest.php | 207 ++ .../GrantType/ClientCredentialsTest.php | 159 ++ .../test/OAuth2/GrantType/ImplicitTest.php | 143 ++ .../test/OAuth2/GrantType/JwtBearerTest.php | 360 ++++ .../OAuth2/GrantType/RefreshTokenTest.php | 204 ++ .../OAuth2/GrantType/UserCredentialsTest.php | 172 ++ .../Controller/AuthorizeControllerTest.php | 182 ++ .../Controller/UserInfoControllerTest.php | 44 + .../GrantType/AuthorizationCodeTest.php | 57 + .../OpenID/ResponseType/CodeIdTokenTest.php | 91 + .../OpenID/ResponseType/IdTokenTest.php | 184 ++ .../OpenID/ResponseType/IdTokenTokenTest.php | 91 + .../OpenID/Storage/AuthorizationCodeTest.php | 95 + .../OAuth2/OpenID/Storage/UserClaimsTest.php | 41 + .../test/OAuth2/RequestTest.php | 98 + .../test/OAuth2/ResponseTest.php | 17 + .../OAuth2/ResponseType/AccessTokenTest.php | 107 ++ .../ResponseType/JwtAccessTokenTest.php | 160 ++ .../test/OAuth2/ScopeTest.php | 42 + .../test/OAuth2/ServerTest.php | 684 +++++++ .../test/OAuth2/Storage/AccessTokenTest.php | 82 + .../OAuth2/Storage/AuthorizationCodeTest.php | 106 + .../OAuth2/Storage/ClientCredentialsTest.php | 28 + .../test/OAuth2/Storage/ClientTest.php | 87 + .../test/OAuth2/Storage/DynamoDBTest.php | 40 + .../OAuth2/Storage/JwtAccessTokenTest.php | 41 + .../test/OAuth2/Storage/JwtBearerTest.php | 25 + .../test/OAuth2/Storage/PdoTest.php | 39 + .../test/OAuth2/Storage/PublicKeyTest.php | 29 + .../test/OAuth2/Storage/RefreshTokenTest.php | 41 + .../test/OAuth2/Storage/ScopeTest.php | 53 + .../OAuth2/Storage/UserCredentialsTest.php | 40 + .../test/OAuth2/TokenType/BearerTest.php | 58 + .../oauth2-server-php/test/bootstrap.php | 12 + .../oauth2-server-php/test/cleanup.php | 15 + .../oauth2-server-php/test/config/keys/id_rsa | 15 + .../test/config/keys/id_rsa.pub | 16 + .../test/config/storage.json | 177 ++ .../test/lib/OAuth2/Request/TestRequest.php | 61 + .../test/lib/OAuth2/Storage/BaseTest.php | 34 + .../test/lib/OAuth2/Storage/Bootstrap.php | 884 +++++++++ .../test/lib/OAuth2/Storage/NullStorage.php | 32 + vendor/codeguy/upload/LICENSE | 19 + vendor/codeguy/upload/README.md | 75 + vendor/codeguy/upload/composer.json | 25 + vendor/codeguy/upload/phpunit.xml | 21 + .../codeguy/upload/src/Upload/Autoloader.php | 77 + .../src/Upload/Exception/UploadException.php | 7 + vendor/codeguy/upload/src/Upload/File.php | 381 ++++ .../upload/src/Upload/Storage/Base.php | 45 + .../upload/src/Upload/Storage/FileSystem.php | 114 ++ .../upload/src/Upload/Validation/Base.php | 75 + .../src/Upload/Validation/Extension.php | 95 + .../upload/src/Upload/Validation/Mimetype.php | 77 + .../upload/src/Upload/Validation/Size.php | 104 + vendor/codeguy/upload/tests/FileTest.php | 176 ++ .../upload/tests/Storage/FileSystemTest.php | 79 + .../upload/tests/Validation/ExtensionTest.php | 43 + .../upload/tests/Validation/MimetypeTest.php | 47 + .../upload/tests/Validation/SizeTest.php | 57 + vendor/codeguy/upload/tests/assets/foo.txt | 6 + vendor/codeguy/upload/tests/bootstrap.php | 3 + vendor/composer/ClassLoader.php | 413 ++++ vendor/composer/LICENSE | 21 + vendor/composer/autoload_classmap.php | 9 + vendor/composer/autoload_files.php | 10 + vendor/composer/autoload_namespaces.php | 15 + vendor/composer/autoload_psr4.php | 14 + vendor/composer/autoload_real.php | 59 + vendor/composer/installed.json | 533 ++++++ .../container-interop/.gitignore | 3 + .../container-interop/LICENSE | 20 + .../container-interop/README.md | 85 + .../container-interop/composer.json | 11 + .../docs/ContainerInterface-meta.md | 114 ++ .../docs/ContainerInterface.md | 153 ++ .../docs/Delegate-lookup-meta.md | 259 +++ .../container-interop/docs/Delegate-lookup.md | 60 + .../docs/images/interoperating_containers.png | Bin 0 -> 35971 bytes .../docs/images/priority.png | Bin 0 -> 22949 bytes .../docs/images/side_by_side_containers.png | Bin 0 -> 22519 bytes .../Interop/Container/ContainerInterface.php | 37 + .../Exception/ContainerException.php | 13 + .../Container/Exception/NotFoundException.php | 13 + vendor/erusev/parsedown/.travis.yml | 16 + vendor/erusev/parsedown/LICENSE.txt | 20 + vendor/erusev/parsedown/Parsedown.php | 1528 +++++++++++++++ vendor/erusev/parsedown/README.md | 57 + vendor/erusev/parsedown/composer.json | 18 + vendor/erusev/parsedown/phpunit.xml.dist | 8 + .../erusev/parsedown/test/CommonMarkTest.php | 74 + .../erusev/parsedown/test/ParsedownTest.php | 159 ++ .../erusev/parsedown/test/TestParsedown.php | 5 + vendor/erusev/parsedown/test/bootstrap.php | 3 + .../parsedown/test/data/aesthetic_table.html | 18 + .../parsedown/test/data/aesthetic_table.md | 4 + .../parsedown/test/data/aligned_table.html | 21 + .../parsedown/test/data/aligned_table.md | 4 + .../parsedown/test/data/atx_heading.html | 9 + .../erusev/parsedown/test/data/atx_heading.md | 17 + .../parsedown/test/data/automatic_link.html | 1 + .../parsedown/test/data/automatic_link.md | 1 + .../parsedown/test/data/block-level_html.html | 12 + .../parsedown/test/data/block-level_html.md | 16 + .../parsedown/test/data/code_block.html | 8 + .../erusev/parsedown/test/data/code_block.md | 10 + .../erusev/parsedown/test/data/code_span.html | 6 + .../erusev/parsedown/test/data/code_span.md | 11 + .../test/data/compound_blockquote.html | 9 + .../test/data/compound_blockquote.md | 10 + .../test/data/compound_emphasis.html | 2 + .../parsedown/test/data/compound_emphasis.md | 4 + .../parsedown/test/data/compound_list.html | 12 + .../parsedown/test/data/compound_list.md | 7 + .../test/data/deeply_nested_list.html | 12 + .../parsedown/test/data/deeply_nested_list.md | 6 + .../erusev/parsedown/test/data/em_strong.html | 8 + .../erusev/parsedown/test/data/em_strong.md | 15 + vendor/erusev/parsedown/test/data/email.html | 1 + vendor/erusev/parsedown/test/data/email.md | 1 + .../erusev/parsedown/test/data/emphasis.html | 8 + vendor/erusev/parsedown/test/data/emphasis.md | 13 + .../erusev/parsedown/test/data/escaping.html | 6 + vendor/erusev/parsedown/test/data/escaping.md | 11 + .../test/data/fenced_code_block.html | 6 + .../parsedown/test/data/fenced_code_block.md | 14 + .../parsedown/test/data/horizontal_rule.html | 5 + .../parsedown/test/data/horizontal_rule.md | 9 + .../parsedown/test/data/html_comment.html | 5 + .../parsedown/test/data/html_comment.md | 8 + .../parsedown/test/data/html_entity.html | 1 + .../erusev/parsedown/test/data/html_entity.md | 1 + .../parsedown/test/data/image_reference.html | 2 + .../parsedown/test/data/image_reference.md | 5 + .../parsedown/test/data/image_title.html | 2 + .../erusev/parsedown/test/data/image_title.md | 3 + .../test/data/implicit_reference.html | 4 + .../parsedown/test/data/implicit_reference.md | 13 + .../parsedown/test/data/inline_link.html | 6 + .../erusev/parsedown/test/data/inline_link.md | 11 + .../test/data/inline_link_title.html | 6 + .../parsedown/test/data/inline_link_title.md | 11 + .../parsedown/test/data/inline_title.html | 1 + .../parsedown/test/data/inline_title.md | 1 + .../parsedown/test/data/lazy_blockquote.html | 6 + .../parsedown/test/data/lazy_blockquote.md | 5 + .../erusev/parsedown/test/data/lazy_list.html | 4 + .../erusev/parsedown/test/data/lazy_list.md | 2 + .../parsedown/test/data/line_break.html | 2 + .../erusev/parsedown/test/data/line_break.md | 2 + .../test/data/multiline_list_paragraph.html | 7 + .../test/data/multiline_list_paragraph.md | 4 + .../test/data/nested_block-level_html.html | 10 + .../test/data/nested_block-level_html.md | 11 + .../parsedown/test/data/ordered_list.html | 13 + .../parsedown/test/data/ordered_list.md | 11 + .../parsedown/test/data/paragraph_list.html | 12 + .../parsedown/test/data/paragraph_list.md | 9 + .../parsedown/test/data/reference_title.html | 2 + .../parsedown/test/data/reference_title.md | 6 + .../test/data/self-closing_html.html | 12 + .../parsedown/test/data/self-closing_html.md | 12 + .../test/data/separated_nested_list.html | 9 + .../test/data/separated_nested_list.md | 4 + .../parsedown/test/data/setext_header.html | 5 + .../parsedown/test/data/setext_header.md | 12 + .../test/data/simple_blockquote.html | 11 + .../parsedown/test/data/simple_blockquote.md | 7 + .../parsedown/test/data/simple_table.html | 37 + .../parsedown/test/data/simple_table.md | 11 + .../parsedown/test/data/span-level_html.html | 5 + .../parsedown/test/data/span-level_html.md | 8 + .../test/data/sparse_dense_list.html | 7 + .../parsedown/test/data/sparse_dense_list.md | 4 + .../parsedown/test/data/sparse_html.html | 8 + .../erusev/parsedown/test/data/sparse_html.md | 8 + .../parsedown/test/data/sparse_list.html | 15 + .../erusev/parsedown/test/data/sparse_list.md | 9 + .../test/data/special_characters.html | 6 + .../parsedown/test/data/special_characters.md | 13 + .../parsedown/test/data/strikethrough.html | 3 + .../parsedown/test/data/strikethrough.md | 5 + .../erusev/parsedown/test/data/strong_em.html | 6 + .../erusev/parsedown/test/data/strong_em.md | 11 + .../test/data/tab-indented_code_block.html | 6 + .../test/data/tab-indented_code_block.md | 6 + .../test/data/table_inline_markdown.html | 22 + .../test/data/table_inline_markdown.md | 5 + .../parsedown/test/data/text_reference.html | 8 + .../parsedown/test/data/text_reference.md | 21 + .../parsedown/test/data/unordered_list.html | 10 + .../parsedown/test/data/unordered_list.md | 8 + .../parsedown/test/data/untidy_table.html | 18 + .../parsedown/test/data/untidy_table.md | 4 + .../parsedown/test/data/url_autolinking.html | 3 + .../parsedown/test/data/url_autolinking.md | 5 + .../parsedown/test/data/whitespace.html | 1 + .../erusev/parsedown/test/data/whitespace.md | 5 + vendor/fguillot/picofeed/LICENSE | 21 + .../fguillot/picofeed/lib/PicoFeed/Base.php | 34 + .../picofeed/lib/PicoFeed/Client/Client.php | 673 +++++++ .../lib/PicoFeed/Client/ClientException.php | 14 + .../picofeed/lib/PicoFeed/Client/Curl.php | 386 ++++ .../PicoFeed/Client/ForbiddenException.php | 10 + .../lib/PicoFeed/Client/HttpHeaders.php | 79 + .../Client/InvalidCertificateException.php | 12 + .../PicoFeed/Client/InvalidUrlException.php | 12 + .../PicoFeed/Client/MaxRedirectException.php | 12 + .../lib/PicoFeed/Client/MaxSizeException.php | 12 + .../picofeed/lib/PicoFeed/Client/Stream.php | 201 ++ .../lib/PicoFeed/Client/TimeoutException.php | 12 + .../PicoFeed/Client/UnauthorizedException.php | 10 + .../picofeed/lib/PicoFeed/Client/Url.php | 290 +++ .../picofeed/lib/PicoFeed/Config/Config.php | 96 + .../lib/PicoFeed/Encoding/Encoding.php | 33 + .../lib/PicoFeed/Filter/Attribute.php | 699 +++++++ .../picofeed/lib/PicoFeed/Filter/Filter.php | 155 ++ .../picofeed/lib/PicoFeed/Filter/Html.php | 243 +++ .../picofeed/lib/PicoFeed/Filter/Tag.php | 215 +++ .../Generator/ContentGeneratorInterface.php | 23 + .../Generator/FileContentGenerator.php | 36 + .../Generator/YoutubeContentGenerator.php | 67 + .../picofeed/lib/PicoFeed/Logging/Logger.php | 114 ++ .../picofeed/lib/PicoFeed/Parser/Atom.php | 364 ++++ .../lib/PicoFeed/Parser/DateParser.php | 126 ++ .../picofeed/lib/PicoFeed/Parser/Feed.php | 314 +++ .../picofeed/lib/PicoFeed/Parser/Item.php | 415 ++++ .../PicoFeed/Parser/MalformedXmlException.php | 12 + .../picofeed/lib/PicoFeed/Parser/Parser.php | 523 +++++ .../lib/PicoFeed/Parser/ParserException.php | 14 + .../picofeed/lib/PicoFeed/Parser/Rss10.php | 277 +++ .../picofeed/lib/PicoFeed/Parser/Rss20.php | 289 +++ .../picofeed/lib/PicoFeed/Parser/Rss91.php | 12 + .../picofeed/lib/PicoFeed/Parser/Rss92.php | 12 + .../PicoFeed/Parser/XmlEntityException.php | 12 + .../lib/PicoFeed/Parser/XmlParser.php | 236 +++ .../lib/PicoFeed/PicoFeedException.php | 14 + .../Processor/ContentFilterProcessor.php | 37 + .../Processor/ContentGeneratorProcessor.php | 49 + .../PicoFeed/Processor/ItemPostProcessor.php | 96 + .../Processor/ItemProcessorInterface.php | 25 + .../PicoFeed/Processor/ScraperProcessor.php | 96 + .../picofeed/lib/PicoFeed/Reader/Favicon.php | 190 ++ .../picofeed/lib/PicoFeed/Reader/Reader.php | 190 ++ .../lib/PicoFeed/Reader/ReaderException.php | 14 + .../Reader/SubscriptionNotFoundException.php | 12 + .../Reader/UnsupportedFeedFormatException.php | 12 + .../lib/PicoFeed/Rules/.blog.lemonde.fr.php | 14 + .../lib/PicoFeed/Rules/.blogs.nytimes.com.php | 15 + .../picofeed/lib/PicoFeed/Rules/.igen.fr.php | 13 + .../lib/PicoFeed/Rules/.nytimes.com.php | 11 + .../lib/PicoFeed/Rules/.over-blog.com.php | 11 + .../lib/PicoFeed/Rules/.phoronix.com.php | 12 + .../lib/PicoFeed/Rules/.slate.com.php | 20 + .../lib/PicoFeed/Rules/.theguardian.com.php | 14 + .../lib/PicoFeed/Rules/.wikipedia.org.php | 29 + .../lib/PicoFeed/Rules/.wired.com.php | 31 + .../picofeed/lib/PicoFeed/Rules/.wsj.com.php | 15 + .../picofeed/lib/PicoFeed/Rules/01net.com.php | 19 + .../lib/PicoFeed/Rules/abstrusegoose.com.php | 9 + .../lib/PicoFeed/Rules/alainonline.net.php | 15 + .../lib/PicoFeed/Rules/aljazeera.com.php | 22 + .../lib/PicoFeed/Rules/allafrica.com.php | 20 + .../PicoFeed/Rules/allgemeine-zeitung.de.php | 24 + .../PicoFeed/Rules/amazingsuperpowers.com.php | 9 + .../lib/PicoFeed/Rules/anythingcomic.com.php | 14 + .../picofeed/lib/PicoFeed/Rules/ap.org.php | 14 + .../lib/PicoFeed/Rules/areadvd.de.php | 11 + .../lib/PicoFeed/Rules/arstechnica.com.php | 23 + .../lib/PicoFeed/Rules/awkwardzombie.com.php | 11 + .../lib/PicoFeed/Rules/bangkokpost.com.php | 21 + .../picofeed/lib/PicoFeed/Rules/bgr.com.php | 16 + .../lib/PicoFeed/Rules/bigfootjustice.com.php | 9 + .../lib/PicoFeed/Rules/bizjournals.com.php | 13 + .../lib/PicoFeed/Rules/blog.fefe.de.php | 14 + .../lib/PicoFeed/Rules/blog.mapillary.com.php | 12 + .../PicoFeed/Rules/buenosairesherald.com.php | 18 + .../lib/PicoFeed/Rules/bunicomic.com.php | 14 + .../lib/PicoFeed/Rules/buttersafe.com.php | 14 + .../lib/PicoFeed/Rules/cad-comic.com.php | 13 + .../Rules/chaoslife.findchaos.com.php | 11 + .../lib/PicoFeed/Rules/cliquerefresh.com.php | 11 + .../picofeed/lib/PicoFeed/Rules/cnet.com.php | 38 + .../lib/PicoFeed/Rules/consomac.fr.php | 14 + .../lib/PicoFeed/Rules/cowbirdsinlove.com.php | 9 + .../lib/PicoFeed/Rules/csmonitor.com.php | 19 + .../lib/PicoFeed/Rules/dailyjs.com.php | 20 + .../lib/PicoFeed/Rules/dailyreporter.com.php | 16 + .../lib/PicoFeed/Rules/dailytech.com.php | 14 + .../lib/PicoFeed/Rules/degroupnews.com.php | 15 + .../lib/PicoFeed/Rules/derstandard.at.php | 15 + .../lib/PicoFeed/Rules/dilbert.com.php | 12 + .../PicoFeed/Rules/discovermagazine.com.php | 18 + .../lib/PicoFeed/Rules/distrowatch.com.php | 14 + .../lib/PicoFeed/Rules/dozodomo.com.php | 16 + .../PicoFeed/Rules/drawingboardcomic.com.php | 16 + .../Rules/encyclopedie.naheulbeuk.com.php | 13 + .../lib/PicoFeed/Rules/endlessorigami.com.php | 9 + .../lib/PicoFeed/Rules/engadget.com.php | 11 + .../PicoFeed/Rules/escapistmagazine.com.php | 46 + .../lib/PicoFeed/Rules/espn.go.com.php | 12 + .../lib/PicoFeed/Rules/exocomics.com.php | 16 + .../lib/PicoFeed/Rules/explosm.net.php | 14 + .../Rules/extrafabulouscomics.com.php | 9 + .../lib/PicoFeed/Rules/fastcodesign.com.php | 14 + .../lib/PicoFeed/Rules/fastcoexist.com.php | 14 + .../lib/PicoFeed/Rules/fastcompany.com.php | 14 + .../lib/PicoFeed/Rules/ffworld.com.php | 14 + .../lib/PicoFeed/Rules/foreignpolicy.com.php | 22 + .../lib/PicoFeed/Rules/fossbytes.com.php | 19 + .../PicoFeed/Rules/fowllanguagecomics.com.php | 11 + .../picofeed/lib/PicoFeed/Rules/geek.com.php | 17 + .../PicoFeed/Rules/gerbilwithajetpack.com.php | 13 + .../lib/PicoFeed/Rules/giantitp.com.php | 13 + .../lib/PicoFeed/Rules/github.com.php | 15 + .../lib/PicoFeed/Rules/gocomics.com.php | 13 + .../picofeed/lib/PicoFeed/Rules/golem.de.php | 13 + .../lib/PicoFeed/Rules/happletea.com.php | 19 + .../picofeed/lib/PicoFeed/Rules/heise.de.php | 13 + .../lib/PicoFeed/Rules/huffingtonpost.com.php | 14 + .../lib/PicoFeed/Rules/imogenquest.net.php | 9 + .../picofeed/lib/PicoFeed/Rules/ing.dk.php | 13 + .../lib/PicoFeed/Rules/invisiblebread.com.php | 9 + .../lib/PicoFeed/Rules/ir.amd.com.php | 11 + .../lib/PicoFeed/Rules/japantimes.co.jp.php | 22 + .../lib/PicoFeed/Rules/japantoday.com.php | 16 + .../lib/PicoFeed/Rules/journaldugeek.com.php | 12 + .../lib/PicoFeed/Rules/jsonline.com.php | 24 + .../picofeed/lib/PicoFeed/Rules/kanpai.fr.php | 14 + .../PicoFeed/Rules/karriere.jobfinder.dk.php | 13 + .../lib/PicoFeed/Rules/koreaherald.com.php | 12 + .../lib/PicoFeed/Rules/koreatimes.php | 16 + .../PicoFeed/Rules/lastplacecomics.com.php | 9 + .../lib/PicoFeed/Rules/lejapon.fr.php | 18 + .../lib/PicoFeed/Rules/lesjoiesducode.fr.php | 14 + .../picofeed/lib/PicoFeed/Rules/lfg.co.php | 13 + .../lib/PicoFeed/Rules/lifehacker.com.php | 19 + .../picofeed/lib/PicoFeed/Rules/linux.org.php | 15 + .../lib/PicoFeed/Rules/linuxinsider.com.php | 21 + .../picofeed/lib/PicoFeed/Rules/lists.php | 14 + .../lib/PicoFeed/Rules/loadingartist.com.php | 9 + .../lib/PicoFeed/Rules/loldwell.com.php | 11 + .../lib/PicoFeed/Rules/lukesurl.com.php | 16 + .../picofeed/lib/PicoFeed/Rules/macg.co.php | 14 + .../picofeed/lib/PicoFeed/Rules/marc.info.php | 14 + .../PicoFeed/Rules/marriedtothesea.com.php | 13 + .../lib/PicoFeed/Rules/marycagle.com.php | 14 + .../Rules/maximumble.thebookofbiff.com.php | 11 + .../lib/PicoFeed/Rules/medium.com.php | 14 + .../lib/PicoFeed/Rules/mercworks.net.php | 18 + .../lib/PicoFeed/Rules/metronieuws.nl.php | 11 + .../lib/PicoFeed/Rules/milwaukeenns.php | 15 + .../picofeed/lib/PicoFeed/Rules/mlb.com.php | 19 + .../Rules/mokepon.smackjeeves.com.php | 11 + .../PicoFeed/Rules/monwindowsphone.com.php | 14 + .../lib/PicoFeed/Rules/mrlovenstein.com.php | 10 + .../lib/PicoFeed/Rules/muckrock.com.php | 17 + .../PicoFeed/Rules/nationaljournal.com.php | 16 + .../lib/PicoFeed/Rules/nature.com.php | 13 + .../picofeed/lib/PicoFeed/Rules/nba.com.php | 16 + .../lib/PicoFeed/Rules/nedroid.com.php | 9 + .../lib/PicoFeed/Rules/networkworld.com.php | 21 + .../lib/PicoFeed/Rules/neustadt-ticker.de.php | 16 + .../lib/PicoFeed/Rules/niceteethcomic.com.php | 11 + .../lib/PicoFeed/Rules/nichtlustig.de.php | 9 + .../picofeed/lib/PicoFeed/Rules/oglaf.com.php | 20 + .../picofeed/lib/PicoFeed/Rules/onhax.net.php | 16 + .../lib/PicoFeed/Rules/onmilwaukee.php | 25 + .../PicoFeed/Rules/openrightsgroup.org.php | 21 + .../lib/PicoFeed/Rules/opensource.com.php | 12 + .../lib/PicoFeed/Rules/optipess.com.php | 9 + .../lib/PicoFeed/Rules/osnews.com.php | 12 + .../lib/PicoFeed/Rules/pastebin.com.php | 14 + .../lib/PicoFeed/Rules/peebleslab.com.php | 10 + .../lib/PicoFeed/Rules/penny-arcade.com.php | 22 + .../lib/PicoFeed/Rules/pixelbeat.org.php | 13 + .../lib/PicoFeed/Rules/plus.google.com.php | 12 + .../lib/PicoFeed/Rules/popstrip.com.php | 9 + .../lib/PicoFeed/Rules/putaindecode.fr.php | 17 + .../lib/PicoFeed/Rules/recode.net.php | 21 + .../PicoFeed/Rules/retractionwatch.com.php | 18 + .../PicoFeed/Rules/rue89.nouvelobs.com.php | 14 + .../lib/PicoFeed/Rules/rugbyrama.fr.php | 20 + .../lib/PicoFeed/Rules/satwcomic.com.php | 14 + .../lib/PicoFeed/Rules/scrumalliance.org.php | 13 + .../lib/PicoFeed/Rules/securityfocus.com.php | 12 + .../PicoFeed/Rules/sentfromthemoon.com.php | 19 + .../lib/PicoFeed/Rules/sitepoint.com.php | 14 + .../lib/PicoFeed/Rules/slashdot.org.php | 12 + .../PicoFeed/Rules/smallhousebliss.com.php | 20 + .../lib/PicoFeed/Rules/smarthomewelt.de.php | 11 + .../PicoFeed/Rules/smashingmagazine.com.php | 11 + .../lib/PicoFeed/Rules/smbc-comics.com.php | 9 + .../lib/PicoFeed/Rules/soundandvision.com.php | 22 + .../lib/PicoFeed/Rules/spiegel.de.php | 12 + .../lib/PicoFeed/Rules/stereophile.com.php | 12 + .../lib/PicoFeed/Rules/stupidfox.net.php | 14 + .../lib/PicoFeed/Rules/subtraction.com.php | 16 + .../picofeed/lib/PicoFeed/Rules/sz.de.php | 11 + .../lib/PicoFeed/Rules/techcrunch.com.php | 16 + .../PicoFeed/Rules/the-ebook-reader.com.php | 12 + .../lib/PicoFeed/Rules/theatlantic.com.php | 23 + .../lib/PicoFeed/Rules/theawkwardyeti.com.php | 9 + .../lib/PicoFeed/Rules/thecodinglove.com.php | 11 + .../PicoFeed/Rules/thedoghousediaries.com.php | 19 + .../lib/PicoFeed/Rules/thegamercat.com.php | 11 + .../lib/PicoFeed/Rules/thehindu.com.php | 20 + .../lib/PicoFeed/Rules/thelocal.se.php | 21 + .../lib/PicoFeed/Rules/themerepublic.net.php | 11 + .../lib/PicoFeed/Rules/themoscowtimes.com.php | 19 + .../lib/PicoFeed/Rules/thenewslens.com.php | 28 + .../lib/PicoFeed/Rules/theodd1sout.com.php | 9 + .../lib/PicoFeed/Rules/theonion.com.php | 13 + .../lib/PicoFeed/Rules/thestandard.com.hk.php | 23 + .../lib/PicoFeed/Rules/threepanelsoul.com.php | 12 + .../Rules/timesofindia.indiatimes.com.php | 15 + .../lib/PicoFeed/Rules/travel-dealz.de.php | 16 + .../lib/PicoFeed/Rules/treehugger.com.php | 15 + .../lib/PicoFeed/Rules/treelobsters.com.php | 9 + .../lib/PicoFeed/Rules/twogag.com.php | 9 + .../PicoFeed/Rules/twokinds.keenspot.com.php | 11 + .../lib/PicoFeed/Rules/undeadly.org.php | 15 + .../picofeed/lib/PicoFeed/Rules/upi.com.php | 16 + .../lib/PicoFeed/Rules/version2.dk.php | 13 + .../lib/PicoFeed/Rules/vgcats.com.php | 16 + .../picofeed/lib/PicoFeed/Rules/vuxml.org.php | 18 + .../lib/PicoFeed/Rules/www.bbc.co.uk.php | 34 + .../lib/PicoFeed/Rules/www.bdgest.com.php | 16 + .../lib/PicoFeed/Rules/www.bgr.in.php | 24 + .../PicoFeed/Rules/www.businessweek.com.php | 16 + .../lib/PicoFeed/Rules/www.cnn.com.php | 25 + .../lib/PicoFeed/Rules/www.developpez.com.php | 22 + .../lib/PicoFeed/Rules/www.egscomics.com.php | 13 + .../Rules/www.fakingnews.firstpost.com.php | 18 + .../lib/PicoFeed/Rules/www.forbes.com.php | 21 + .../PicoFeed/Rules/www.franceculture.fr.php | 14 + .../Rules/www.futura-sciences.com.php | 20 + .../PicoFeed/Rules/www.geekculture.com.php | 13 + .../lib/PicoFeed/Rules/www.howtogeek.com.php | 15 + .../lib/PicoFeed/Rules/www.lepoint.fr.php | 19 + .../PicoFeed/Rules/www.lesnumeriques.com.php | 26 + .../lib/PicoFeed/Rules/www.mac4ever.com.php | 14 + .../lib/PicoFeed/Rules/www.makeuseof.com.php | 19 + .../Rules/www.monsieur-le-chien.fr.php | 11 + .../lib/PicoFeed/Rules/www.npr.org.php | 21 + .../lib/PicoFeed/Rules/www.numerama.com.php | 16 + .../lib/PicoFeed/Rules/www.oneindia.com.php | 15 + .../lib/PicoFeed/Rules/www.pcinpact.com.php | 14 + .../Rules/www.pseudo-sciences.org.php | 17 + .../lib/PicoFeed/Rules/www.sciencemag.org.php | 16 + .../lib/PicoFeed/Rules/www.slate.fr.php | 20 + .../PicoFeed/Rules/www.universfreebox.com.php | 16 + .../lib/PicoFeed/Rules/www.zeit.de.php | 41 + .../picofeed/lib/PicoFeed/Rules/xkcd.com.php | 9 + .../picofeed/lib/PicoFeed/Rules/zdnet.com.php | 24 + .../lib/PicoFeed/Scraper/CandidateParser.php | 273 +++ .../lib/PicoFeed/Scraper/ParserInterface.php | 13 + .../lib/PicoFeed/Scraper/RuleLoader.php | 107 ++ .../lib/PicoFeed/Scraper/RuleParser.php | 83 + .../picofeed/lib/PicoFeed/Scraper/Scraper.php | 267 +++ .../PicoFeed/Serialization/Subscription.php | 175 ++ .../Serialization/SubscriptionList.php | 75 + .../Serialization/SubscriptionListBuilder.php | 204 ++ .../Serialization/SubscriptionListParser.php | 100 + .../Serialization/SubscriptionParser.php | 142 ++ .../PicoFeed/Syndication/AtomFeedBuilder.php | 65 + .../lib/PicoFeed/Syndication/AtomHelper.php | 139 ++ .../PicoFeed/Syndication/AtomItemBuilder.php | 63 + .../lib/PicoFeed/Syndication/FeedBuilder.php | 185 ++ .../lib/PicoFeed/Syndication/ItemBuilder.php | 209 ++ .../PicoFeed/Syndication/Rss20FeedBuilder.php | 76 + .../lib/PicoFeed/Syndication/Rss20Helper.php | 115 ++ .../PicoFeed/Syndication/Rss20ItemBuilder.php | 67 + vendor/fguillot/picofeed/picofeed | 125 ++ vendor/nikic/fast-route/.travis.yml | 12 + vendor/nikic/fast-route/LICENSE | 31 + vendor/nikic/fast-route/README.md | 209 ++ vendor/nikic/fast-route/composer.json | 21 + vendor/nikic/fast-route/phpunit.xml | 24 + .../fast-route/src/BadRouteException.php | 6 + vendor/nikic/fast-route/src/DataGenerator.php | 25 + .../src/DataGenerator/CharCountBased.php | 28 + .../src/DataGenerator/GroupCountBased.php | 28 + .../src/DataGenerator/GroupPosBased.php | 25 + .../src/DataGenerator/MarkBased.php | 25 + .../src/DataGenerator/RegexBasedAbstract.php | 144 ++ vendor/nikic/fast-route/src/Dispatcher.php | 25 + .../src/Dispatcher/CharCountBased.php | 28 + .../src/Dispatcher/GroupCountBased.php | 28 + .../src/Dispatcher/GroupPosBased.php | 30 + .../fast-route/src/Dispatcher/MarkBased.php | 28 + .../src/Dispatcher/RegexBasedAbstract.php | 62 + vendor/nikic/fast-route/src/Route.php | 38 + .../nikic/fast-route/src/RouteCollector.php | 46 + vendor/nikic/fast-route/src/RouteParser.php | 36 + .../nikic/fast-route/src/RouteParser/Std.php | 77 + vendor/nikic/fast-route/src/bootstrap.php | 12 + vendor/nikic/fast-route/src/functions.php | 70 + .../test/Dispatcher/CharCountBasedTest.php | 13 + .../test/Dispatcher/DispatcherTest.php | 494 +++++ .../test/Dispatcher/GroupCountBasedTest.php | 13 + .../test/Dispatcher/GroupPosBasedTest.php | 13 + .../test/Dispatcher/MarkBasedTest.php | 20 + .../fast-route/test/RouteParser/StdTest.php | 114 ++ vendor/nikic/fast-route/test/bootstrap.php | 11 + vendor/pimple/pimple/.gitignore | 3 + vendor/pimple/pimple/.travis.yml | 32 + vendor/pimple/pimple/CHANGELOG | 35 + vendor/pimple/pimple/LICENSE | 19 + vendor/pimple/pimple/README.rst | 201 ++ vendor/pimple/pimple/composer.json | 25 + vendor/pimple/pimple/ext/pimple/.gitignore | 30 + vendor/pimple/pimple/ext/pimple/README.md | 12 + vendor/pimple/pimple/ext/pimple/config.m4 | 63 + vendor/pimple/pimple/ext/pimple/config.w32 | 13 + vendor/pimple/pimple/ext/pimple/php_pimple.h | 121 ++ vendor/pimple/pimple/ext/pimple/pimple.c | 922 +++++++++ .../pimple/pimple/ext/pimple/pimple_compat.h | 81 + .../pimple/pimple/ext/pimple/tests/001.phpt | 45 + .../pimple/pimple/ext/pimple/tests/002.phpt | 15 + .../pimple/pimple/ext/pimple/tests/003.phpt | 16 + .../pimple/pimple/ext/pimple/tests/004.phpt | 30 + .../pimple/pimple/ext/pimple/tests/005.phpt | 27 + .../pimple/pimple/ext/pimple/tests/006.phpt | 51 + .../pimple/pimple/ext/pimple/tests/007.phpt | 22 + .../pimple/pimple/ext/pimple/tests/008.phpt | 29 + .../pimple/pimple/ext/pimple/tests/009.phpt | 13 + .../pimple/pimple/ext/pimple/tests/010.phpt | 45 + .../pimple/pimple/ext/pimple/tests/011.phpt | 19 + .../pimple/pimple/ext/pimple/tests/012.phpt | 28 + .../pimple/pimple/ext/pimple/tests/013.phpt | 33 + .../pimple/pimple/ext/pimple/tests/014.phpt | 30 + .../pimple/pimple/ext/pimple/tests/015.phpt | 17 + .../pimple/pimple/ext/pimple/tests/016.phpt | 24 + .../pimple/pimple/ext/pimple/tests/017.phpt | 17 + .../pimple/pimple/ext/pimple/tests/017_1.phpt | 17 + .../pimple/pimple/ext/pimple/tests/018.phpt | 23 + .../pimple/pimple/ext/pimple/tests/019.phpt | 18 + .../pimple/pimple/ext/pimple/tests/bench.phpb | 51 + .../pimple/ext/pimple/tests/bench_shared.phpb | 25 + vendor/pimple/pimple/phpunit.xml.dist | 14 + vendor/pimple/pimple/src/Pimple/Container.php | 282 +++ .../src/Pimple/ServiceProviderInterface.php | 46 + .../src/Pimple/Tests/Fixtures/Invokable.php | 38 + .../Pimple/Tests/Fixtures/NonInvokable.php | 34 + .../Tests/Fixtures/PimpleServiceProvider.php | 54 + .../src/Pimple/Tests/Fixtures/Service.php | 35 + .../PimpleServiceProviderInterfaceTest.php | 76 + .../pimple/src/Pimple/Tests/PimpleTest.php | 440 +++++ vendor/psr/http-message/LICENSE | 19 + vendor/psr/http-message/README.md | 13 + vendor/psr/http-message/composer.json | 25 + .../psr/http-message/src/MessageInterface.php | 187 ++ .../psr/http-message/src/RequestInterface.php | 129 ++ .../http-message/src/ResponseInterface.php | 68 + .../src/ServerRequestInterface.php | 261 +++ .../psr/http-message/src/StreamInterface.php | 158 ++ .../src/UploadedFileInterface.php | 123 ++ vendor/psr/http-message/src/UriInterface.php | 323 ++++ vendor/slim/slim/CONTRIBUTING.md | 20 + vendor/slim/slim/LICENSE.md | 19 + vendor/slim/slim/README.md | 84 + vendor/slim/slim/Slim/App.php | 635 ++++++ vendor/slim/slim/Slim/CallableResolver.php | 87 + .../slim/Slim/CallableResolverAwareTrait.php | 47 + vendor/slim/slim/Slim/Collection.php | 204 ++ vendor/slim/slim/Slim/Container.php | 151 ++ .../slim/Slim/DefaultServicesProvider.php | 197 ++ vendor/slim/slim/Slim/DeferredCallable.php | 39 + .../ContainerValueNotFoundException.php | 20 + .../Exception/MethodNotAllowedException.php | 45 + .../slim/Slim/Exception/NotFoundException.php | 14 + .../slim/Slim/Exception/SlimException.php | 69 + vendor/slim/slim/Slim/Handlers/Error.php | 300 +++ vendor/slim/slim/Slim/Handlers/NotAllowed.php | 173 ++ vendor/slim/slim/Slim/Handlers/NotFound.php | 157 ++ vendor/slim/slim/Slim/Handlers/PhpError.php | 303 +++ .../Handlers/Strategies/RequestResponse.php | 43 + .../Strategies/RequestResponseArgs.php | 42 + vendor/slim/slim/Slim/Http/Body.php | 22 + vendor/slim/slim/Slim/Http/Cookies.php | 195 ++ vendor/slim/slim/Slim/Http/Environment.php | 52 + vendor/slim/slim/Slim/Http/Headers.php | 197 ++ vendor/slim/slim/Slim/Http/Message.php | 295 +++ vendor/slim/slim/Slim/Http/Request.php | 1150 +++++++++++ vendor/slim/slim/Slim/Http/RequestBody.php | 27 + vendor/slim/slim/Slim/Http/Response.php | 461 +++++ vendor/slim/slim/Slim/Http/Stream.php | 409 ++++ vendor/slim/slim/Slim/Http/UploadedFile.php | 327 ++++ vendor/slim/slim/Slim/Http/Uri.php | 821 ++++++++ .../Interfaces/CallableResolverInterface.php | 27 + .../Slim/Interfaces/CollectionInterface.php | 32 + .../Slim/Interfaces/Http/CookiesInterface.php | 23 + .../Interfaces/Http/EnvironmentInterface.php | 20 + .../Slim/Interfaces/Http/HeadersInterface.php | 24 + .../InvocationStrategyInterface.php | 35 + .../Slim/Interfaces/RouteGroupInterface.php | 46 + .../slim/Slim/Interfaces/RouteInterface.php | 129 ++ .../slim/Slim/Interfaces/RouterInterface.php | 107 ++ .../slim/slim/Slim/MiddlewareAwareTrait.php | 120 ++ vendor/slim/slim/Slim/Routable.php | 96 + vendor/slim/slim/Slim/Route.php | 357 ++++ vendor/slim/slim/Slim/RouteGroup.php | 47 + vendor/slim/slim/Slim/Router.php | 361 ++++ vendor/slim/slim/composer.json | 54 + vendor/slim/slim/example/.htaccess | 12 + vendor/slim/slim/example/index.php | 45 + vendor/symfony/yaml/.gitignore | 3 + vendor/symfony/yaml/CHANGELOG.md | 34 + vendor/symfony/yaml/Dumper.php | 77 + vendor/symfony/yaml/Escaper.php | 99 + .../symfony/yaml/Exception/DumpException.php | 21 + .../yaml/Exception/ExceptionInterface.php | 21 + .../symfony/yaml/Exception/ParseException.php | 141 ++ .../yaml/Exception/RuntimeException.php | 21 + vendor/symfony/yaml/Inline.php | 568 ++++++ vendor/symfony/yaml/LICENSE | 19 + vendor/symfony/yaml/Parser.php | 778 ++++++++ vendor/symfony/yaml/README.md | 13 + vendor/symfony/yaml/Tests/DumperTest.php | 254 +++ .../yaml/Tests/Fixtures/YtsAnchorAlias.yml | 31 + .../yaml/Tests/Fixtures/YtsBasicTests.yml | 202 ++ .../yaml/Tests/Fixtures/YtsBlockMapping.yml | 51 + .../Tests/Fixtures/YtsDocumentSeparator.yml | 85 + .../yaml/Tests/Fixtures/YtsErrorTests.yml | 25 + .../Tests/Fixtures/YtsFlowCollections.yml | 60 + .../yaml/Tests/Fixtures/YtsFoldedScalars.yml | 176 ++ .../Tests/Fixtures/YtsNullsAndEmpties.yml | 45 + .../Fixtures/YtsSpecificationExamples.yml | 1697 +++++++++++++++++ .../yaml/Tests/Fixtures/YtsTypeTransfers.yml | 244 +++ .../yaml/Tests/Fixtures/embededPhp.yml | 1 + .../yaml/Tests/Fixtures/escapedCharacters.yml | 155 ++ vendor/symfony/yaml/Tests/Fixtures/index.yml | 18 + .../yaml/Tests/Fixtures/sfComments.yml | 76 + .../symfony/yaml/Tests/Fixtures/sfCompact.yml | 159 ++ .../yaml/Tests/Fixtures/sfMergeKey.yml | 58 + .../symfony/yaml/Tests/Fixtures/sfObjects.yml | 11 + .../symfony/yaml/Tests/Fixtures/sfQuotes.yml | 33 + .../symfony/yaml/Tests/Fixtures/sfTests.yml | 149 ++ .../Tests/Fixtures/unindentedCollections.yml | 82 + vendor/symfony/yaml/Tests/InlineTest.php | 429 +++++ .../symfony/yaml/Tests/ParseExceptionTest.php | 33 + vendor/symfony/yaml/Tests/ParserTest.php | 1091 +++++++++++ vendor/symfony/yaml/Tests/YamlTest.php | 43 + vendor/symfony/yaml/Unescaper.php | 142 ++ vendor/symfony/yaml/Yaml.php | 73 + vendor/symfony/yaml/composer.json | 33 + vendor/symfony/yaml/phpunit.xml.dist | 28 + vendor/zendframework/zendxml/.gitignore | 5 + vendor/zendframework/zendxml/.travis.yml | 43 + vendor/zendframework/zendxml/CHANGELOG.md | 24 + vendor/zendframework/zendxml/LICENSE.md | 12 + vendor/zendframework/zendxml/README.md | 50 + vendor/zendframework/zendxml/composer.json | 40 + .../ZendXml/Exception/ExceptionInterface.php | 14 + .../Exception/InvalidArgumentException.php | 17 + .../ZendXml/Exception/RuntimeException.php | 17 + .../zendxml/library/ZendXml/Security.php | 374 ++++ .../zendframework/zendxml/tests/Bootstrap.php | 92 + .../tests/ZendXmlTest/MultibyteTest.php | 125 ++ .../tests/ZendXmlTest/SecurityTest.php | 135 ++ .../zendxml/tests/phpunit.xml.dist | 27 + 745 files changed, 56017 insertions(+), 1 deletion(-) create mode 100644 vendor/autoload.php create mode 120000 vendor/bin/picofeed create mode 100644 vendor/bshaffer/oauth2-server-php/.gitignore create mode 100644 vendor/bshaffer/oauth2-server-php/.travis.yml create mode 100644 vendor/bshaffer/oauth2-server-php/CHANGELOG.md create mode 100644 vendor/bshaffer/oauth2-server-php/LICENSE create mode 100644 vendor/bshaffer/oauth2-server-php/README.md create mode 100644 vendor/bshaffer/oauth2-server-php/composer.json create mode 100644 vendor/bshaffer/oauth2-server-php/phpunit.xml create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Autoloader.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/HttpBasic.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeController.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeControllerInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceController.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceControllerInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenController.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenControllerInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/EncryptionInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/FirebaseJwt.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/Jwt.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/AuthorizationCode.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/ClientCredentials.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/GrantTypeInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/JwtBearer.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/RefreshToken.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/UserCredentials.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeController.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoController.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/GrantType/AuthorizationCode.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdToken.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdToken.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenToken.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/UserClaimsInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Request.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/RequestInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Response.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessToken.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessTokenInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCode.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCodeInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/JwtAccessToken.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/ResponseTypeInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Scope.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/ScopeInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Server.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AccessTokenInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AuthorizationCodeInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Cassandra.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientCredentialsInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientInterface.php create mode 100755 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/CouchbaseDB.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/DynamoDB.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessToken.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessTokenInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtBearerInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Memory.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Mongo.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Pdo.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/PublicKeyInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Redis.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/RefreshTokenInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ScopeInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/UserCredentialsInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Bearer.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Mac.php create mode 100644 vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/TokenTypeInterface.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/AutoloadTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/AuthorizeControllerTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/ResourceControllerTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/TokenControllerTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/FirebaseJwtTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/JwtTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/AuthorizationCodeTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ClientCredentialsTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ImplicitTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/JwtBearerTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/RefreshTokenTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/UserCredentialsTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/UserClaimsTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/RequestTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseType/AccessTokenTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseType/JwtAccessTokenTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/ScopeTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/ServerTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/AccessTokenTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/AuthorizationCodeTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ClientCredentialsTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ClientTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/DynamoDBTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/JwtAccessTokenTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/JwtBearerTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/PdoTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/PublicKeyTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/RefreshTokenTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ScopeTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/UserCredentialsTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/OAuth2/TokenType/BearerTest.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/bootstrap.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/cleanup.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/config/keys/id_rsa create mode 100644 vendor/bshaffer/oauth2-server-php/test/config/keys/id_rsa.pub create mode 100644 vendor/bshaffer/oauth2-server-php/test/config/storage.json create mode 100644 vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Request/TestRequest.php create mode 100755 vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/BaseTest.php create mode 100755 vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/Bootstrap.php create mode 100644 vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/NullStorage.php create mode 100644 vendor/codeguy/upload/LICENSE create mode 100644 vendor/codeguy/upload/README.md create mode 100644 vendor/codeguy/upload/composer.json create mode 100644 vendor/codeguy/upload/phpunit.xml create mode 100644 vendor/codeguy/upload/src/Upload/Autoloader.php create mode 100644 vendor/codeguy/upload/src/Upload/Exception/UploadException.php create mode 100644 vendor/codeguy/upload/src/Upload/File.php create mode 100644 vendor/codeguy/upload/src/Upload/Storage/Base.php create mode 100644 vendor/codeguy/upload/src/Upload/Storage/FileSystem.php create mode 100644 vendor/codeguy/upload/src/Upload/Validation/Base.php create mode 100644 vendor/codeguy/upload/src/Upload/Validation/Extension.php create mode 100644 vendor/codeguy/upload/src/Upload/Validation/Mimetype.php create mode 100644 vendor/codeguy/upload/src/Upload/Validation/Size.php create mode 100644 vendor/codeguy/upload/tests/FileTest.php create mode 100644 vendor/codeguy/upload/tests/Storage/FileSystemTest.php create mode 100644 vendor/codeguy/upload/tests/Validation/ExtensionTest.php create mode 100644 vendor/codeguy/upload/tests/Validation/MimetypeTest.php create mode 100644 vendor/codeguy/upload/tests/Validation/SizeTest.php create mode 100644 vendor/codeguy/upload/tests/assets/foo.txt create mode 100644 vendor/codeguy/upload/tests/bootstrap.php create mode 100644 vendor/composer/ClassLoader.php create mode 100644 vendor/composer/LICENSE create mode 100644 vendor/composer/autoload_classmap.php create mode 100644 vendor/composer/autoload_files.php create mode 100644 vendor/composer/autoload_namespaces.php create mode 100644 vendor/composer/autoload_psr4.php create mode 100644 vendor/composer/autoload_real.php create mode 100644 vendor/composer/installed.json create mode 100644 vendor/container-interop/container-interop/.gitignore create mode 100644 vendor/container-interop/container-interop/LICENSE create mode 100644 vendor/container-interop/container-interop/README.md create mode 100644 vendor/container-interop/container-interop/composer.json create mode 100644 vendor/container-interop/container-interop/docs/ContainerInterface-meta.md create mode 100644 vendor/container-interop/container-interop/docs/ContainerInterface.md create mode 100644 vendor/container-interop/container-interop/docs/Delegate-lookup-meta.md create mode 100644 vendor/container-interop/container-interop/docs/Delegate-lookup.md create mode 100644 vendor/container-interop/container-interop/docs/images/interoperating_containers.png create mode 100644 vendor/container-interop/container-interop/docs/images/priority.png create mode 100644 vendor/container-interop/container-interop/docs/images/side_by_side_containers.png create mode 100644 vendor/container-interop/container-interop/src/Interop/Container/ContainerInterface.php create mode 100644 vendor/container-interop/container-interop/src/Interop/Container/Exception/ContainerException.php create mode 100644 vendor/container-interop/container-interop/src/Interop/Container/Exception/NotFoundException.php create mode 100644 vendor/erusev/parsedown/.travis.yml create mode 100644 vendor/erusev/parsedown/LICENSE.txt create mode 100755 vendor/erusev/parsedown/Parsedown.php create mode 100644 vendor/erusev/parsedown/README.md create mode 100644 vendor/erusev/parsedown/composer.json create mode 100644 vendor/erusev/parsedown/phpunit.xml.dist create mode 100644 vendor/erusev/parsedown/test/CommonMarkTest.php create mode 100644 vendor/erusev/parsedown/test/ParsedownTest.php create mode 100644 vendor/erusev/parsedown/test/TestParsedown.php create mode 100644 vendor/erusev/parsedown/test/bootstrap.php create mode 100644 vendor/erusev/parsedown/test/data/aesthetic_table.html create mode 100644 vendor/erusev/parsedown/test/data/aesthetic_table.md create mode 100644 vendor/erusev/parsedown/test/data/aligned_table.html create mode 100644 vendor/erusev/parsedown/test/data/aligned_table.md create mode 100644 vendor/erusev/parsedown/test/data/atx_heading.html create mode 100644 vendor/erusev/parsedown/test/data/atx_heading.md create mode 100644 vendor/erusev/parsedown/test/data/automatic_link.html create mode 100644 vendor/erusev/parsedown/test/data/automatic_link.md create mode 100644 vendor/erusev/parsedown/test/data/block-level_html.html create mode 100644 vendor/erusev/parsedown/test/data/block-level_html.md create mode 100644 vendor/erusev/parsedown/test/data/code_block.html create mode 100644 vendor/erusev/parsedown/test/data/code_block.md create mode 100644 vendor/erusev/parsedown/test/data/code_span.html create mode 100644 vendor/erusev/parsedown/test/data/code_span.md create mode 100644 vendor/erusev/parsedown/test/data/compound_blockquote.html create mode 100644 vendor/erusev/parsedown/test/data/compound_blockquote.md create mode 100644 vendor/erusev/parsedown/test/data/compound_emphasis.html create mode 100644 vendor/erusev/parsedown/test/data/compound_emphasis.md create mode 100644 vendor/erusev/parsedown/test/data/compound_list.html create mode 100644 vendor/erusev/parsedown/test/data/compound_list.md create mode 100644 vendor/erusev/parsedown/test/data/deeply_nested_list.html create mode 100644 vendor/erusev/parsedown/test/data/deeply_nested_list.md create mode 100644 vendor/erusev/parsedown/test/data/em_strong.html create mode 100644 vendor/erusev/parsedown/test/data/em_strong.md create mode 100644 vendor/erusev/parsedown/test/data/email.html create mode 100644 vendor/erusev/parsedown/test/data/email.md create mode 100644 vendor/erusev/parsedown/test/data/emphasis.html create mode 100644 vendor/erusev/parsedown/test/data/emphasis.md create mode 100644 vendor/erusev/parsedown/test/data/escaping.html create mode 100644 vendor/erusev/parsedown/test/data/escaping.md create mode 100644 vendor/erusev/parsedown/test/data/fenced_code_block.html create mode 100644 vendor/erusev/parsedown/test/data/fenced_code_block.md create mode 100644 vendor/erusev/parsedown/test/data/horizontal_rule.html create mode 100644 vendor/erusev/parsedown/test/data/horizontal_rule.md create mode 100644 vendor/erusev/parsedown/test/data/html_comment.html create mode 100644 vendor/erusev/parsedown/test/data/html_comment.md create mode 100644 vendor/erusev/parsedown/test/data/html_entity.html create mode 100644 vendor/erusev/parsedown/test/data/html_entity.md create mode 100644 vendor/erusev/parsedown/test/data/image_reference.html create mode 100644 vendor/erusev/parsedown/test/data/image_reference.md create mode 100644 vendor/erusev/parsedown/test/data/image_title.html create mode 100644 vendor/erusev/parsedown/test/data/image_title.md create mode 100644 vendor/erusev/parsedown/test/data/implicit_reference.html create mode 100644 vendor/erusev/parsedown/test/data/implicit_reference.md create mode 100644 vendor/erusev/parsedown/test/data/inline_link.html create mode 100644 vendor/erusev/parsedown/test/data/inline_link.md create mode 100644 vendor/erusev/parsedown/test/data/inline_link_title.html create mode 100644 vendor/erusev/parsedown/test/data/inline_link_title.md create mode 100644 vendor/erusev/parsedown/test/data/inline_title.html create mode 100644 vendor/erusev/parsedown/test/data/inline_title.md create mode 100644 vendor/erusev/parsedown/test/data/lazy_blockquote.html create mode 100644 vendor/erusev/parsedown/test/data/lazy_blockquote.md create mode 100644 vendor/erusev/parsedown/test/data/lazy_list.html create mode 100644 vendor/erusev/parsedown/test/data/lazy_list.md create mode 100644 vendor/erusev/parsedown/test/data/line_break.html create mode 100644 vendor/erusev/parsedown/test/data/line_break.md create mode 100644 vendor/erusev/parsedown/test/data/multiline_list_paragraph.html create mode 100644 vendor/erusev/parsedown/test/data/multiline_list_paragraph.md create mode 100644 vendor/erusev/parsedown/test/data/nested_block-level_html.html create mode 100644 vendor/erusev/parsedown/test/data/nested_block-level_html.md create mode 100644 vendor/erusev/parsedown/test/data/ordered_list.html create mode 100644 vendor/erusev/parsedown/test/data/ordered_list.md create mode 100644 vendor/erusev/parsedown/test/data/paragraph_list.html create mode 100644 vendor/erusev/parsedown/test/data/paragraph_list.md create mode 100644 vendor/erusev/parsedown/test/data/reference_title.html create mode 100644 vendor/erusev/parsedown/test/data/reference_title.md create mode 100644 vendor/erusev/parsedown/test/data/self-closing_html.html create mode 100644 vendor/erusev/parsedown/test/data/self-closing_html.md create mode 100644 vendor/erusev/parsedown/test/data/separated_nested_list.html create mode 100644 vendor/erusev/parsedown/test/data/separated_nested_list.md create mode 100644 vendor/erusev/parsedown/test/data/setext_header.html create mode 100644 vendor/erusev/parsedown/test/data/setext_header.md create mode 100644 vendor/erusev/parsedown/test/data/simple_blockquote.html create mode 100644 vendor/erusev/parsedown/test/data/simple_blockquote.md create mode 100644 vendor/erusev/parsedown/test/data/simple_table.html create mode 100644 vendor/erusev/parsedown/test/data/simple_table.md create mode 100644 vendor/erusev/parsedown/test/data/span-level_html.html create mode 100644 vendor/erusev/parsedown/test/data/span-level_html.md create mode 100644 vendor/erusev/parsedown/test/data/sparse_dense_list.html create mode 100644 vendor/erusev/parsedown/test/data/sparse_dense_list.md create mode 100644 vendor/erusev/parsedown/test/data/sparse_html.html create mode 100644 vendor/erusev/parsedown/test/data/sparse_html.md create mode 100644 vendor/erusev/parsedown/test/data/sparse_list.html create mode 100644 vendor/erusev/parsedown/test/data/sparse_list.md create mode 100644 vendor/erusev/parsedown/test/data/special_characters.html create mode 100644 vendor/erusev/parsedown/test/data/special_characters.md create mode 100644 vendor/erusev/parsedown/test/data/strikethrough.html create mode 100644 vendor/erusev/parsedown/test/data/strikethrough.md create mode 100644 vendor/erusev/parsedown/test/data/strong_em.html create mode 100644 vendor/erusev/parsedown/test/data/strong_em.md create mode 100644 vendor/erusev/parsedown/test/data/tab-indented_code_block.html create mode 100644 vendor/erusev/parsedown/test/data/tab-indented_code_block.md create mode 100644 vendor/erusev/parsedown/test/data/table_inline_markdown.html create mode 100644 vendor/erusev/parsedown/test/data/table_inline_markdown.md create mode 100644 vendor/erusev/parsedown/test/data/text_reference.html create mode 100644 vendor/erusev/parsedown/test/data/text_reference.md create mode 100644 vendor/erusev/parsedown/test/data/unordered_list.html create mode 100644 vendor/erusev/parsedown/test/data/unordered_list.md create mode 100644 vendor/erusev/parsedown/test/data/untidy_table.html create mode 100644 vendor/erusev/parsedown/test/data/untidy_table.md create mode 100644 vendor/erusev/parsedown/test/data/url_autolinking.html create mode 100644 vendor/erusev/parsedown/test/data/url_autolinking.md create mode 100644 vendor/erusev/parsedown/test/data/whitespace.html create mode 100644 vendor/erusev/parsedown/test/data/whitespace.md create mode 100644 vendor/fguillot/picofeed/LICENSE create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Base.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Client/Client.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Client/ClientException.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Client/Curl.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Client/ForbiddenException.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Client/HttpHeaders.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Client/InvalidCertificateException.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Client/InvalidUrlException.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Client/MaxRedirectException.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Client/MaxSizeException.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Client/Stream.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Client/TimeoutException.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Client/UnauthorizedException.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Client/Url.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Config/Config.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Encoding/Encoding.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Filter/Attribute.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Filter/Filter.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Filter/Html.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Filter/Tag.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Generator/ContentGeneratorInterface.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Generator/FileContentGenerator.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Generator/YoutubeContentGenerator.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Logging/Logger.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Parser/Atom.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Parser/DateParser.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Parser/Feed.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Parser/Item.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Parser/MalformedXmlException.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Parser/Parser.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Parser/ParserException.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Parser/Rss10.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Parser/Rss20.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Parser/Rss91.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Parser/Rss92.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Parser/XmlEntityException.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Parser/XmlParser.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/PicoFeedException.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Processor/ContentFilterProcessor.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Processor/ContentGeneratorProcessor.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Processor/ItemPostProcessor.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Processor/ItemProcessorInterface.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Processor/ScraperProcessor.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Reader/Favicon.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Reader/Reader.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Reader/ReaderException.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Reader/SubscriptionNotFoundException.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Reader/UnsupportedFeedFormatException.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/.blog.lemonde.fr.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/.blogs.nytimes.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/.igen.fr.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/.nytimes.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/.over-blog.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/.phoronix.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/.slate.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/.theguardian.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/.wikipedia.org.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/.wired.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/.wsj.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/01net.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/abstrusegoose.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/alainonline.net.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/aljazeera.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/allafrica.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/allgemeine-zeitung.de.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/amazingsuperpowers.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/anythingcomic.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/ap.org.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/areadvd.de.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/arstechnica.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/awkwardzombie.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/bangkokpost.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/bgr.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/bigfootjustice.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/bizjournals.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/blog.fefe.de.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/blog.mapillary.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/buenosairesherald.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/bunicomic.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/buttersafe.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/cad-comic.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/chaoslife.findchaos.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/cliquerefresh.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/cnet.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/consomac.fr.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/cowbirdsinlove.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/csmonitor.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/dailyjs.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/dailyreporter.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/dailytech.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/degroupnews.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/derstandard.at.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/dilbert.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/discovermagazine.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/distrowatch.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/dozodomo.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/drawingboardcomic.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/encyclopedie.naheulbeuk.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/endlessorigami.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/engadget.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/escapistmagazine.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/espn.go.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/exocomics.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/explosm.net.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/extrafabulouscomics.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/fastcodesign.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/fastcoexist.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/fastcompany.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/ffworld.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/foreignpolicy.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/fossbytes.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/fowllanguagecomics.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/geek.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/gerbilwithajetpack.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/giantitp.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/github.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/gocomics.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/golem.de.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/happletea.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/heise.de.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/huffingtonpost.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/imogenquest.net.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/ing.dk.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/invisiblebread.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/ir.amd.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/japantimes.co.jp.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/japantoday.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/journaldugeek.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/jsonline.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/kanpai.fr.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/karriere.jobfinder.dk.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/koreaherald.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/koreatimes.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/lastplacecomics.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/lejapon.fr.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/lesjoiesducode.fr.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/lfg.co.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/lifehacker.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/linux.org.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/linuxinsider.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/lists.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/loadingartist.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/loldwell.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/lukesurl.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/macg.co.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/marc.info.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/marriedtothesea.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/marycagle.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/maximumble.thebookofbiff.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/medium.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/mercworks.net.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/metronieuws.nl.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/milwaukeenns.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/mlb.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/mokepon.smackjeeves.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/monwindowsphone.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/mrlovenstein.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/muckrock.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/nationaljournal.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/nature.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/nba.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/nedroid.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/networkworld.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/neustadt-ticker.de.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/niceteethcomic.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/nichtlustig.de.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/oglaf.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/onhax.net.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/onmilwaukee.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/openrightsgroup.org.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/opensource.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/optipess.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/osnews.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/pastebin.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/peebleslab.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/penny-arcade.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/pixelbeat.org.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/plus.google.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/popstrip.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/putaindecode.fr.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/recode.net.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/retractionwatch.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/rue89.nouvelobs.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/rugbyrama.fr.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/satwcomic.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/scrumalliance.org.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/securityfocus.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/sentfromthemoon.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/sitepoint.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/slashdot.org.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/smallhousebliss.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/smarthomewelt.de.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/smashingmagazine.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/smbc-comics.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/soundandvision.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/spiegel.de.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/stereophile.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/stupidfox.net.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/subtraction.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/sz.de.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/techcrunch.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/the-ebook-reader.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/theatlantic.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/theawkwardyeti.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/thecodinglove.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/thedoghousediaries.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/thegamercat.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/thehindu.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/thelocal.se.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/themerepublic.net.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/themoscowtimes.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/thenewslens.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/theodd1sout.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/theonion.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/thestandard.com.hk.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/threepanelsoul.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/timesofindia.indiatimes.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/travel-dealz.de.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/treehugger.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/treelobsters.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/twogag.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/twokinds.keenspot.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/undeadly.org.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/upi.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/version2.dk.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/vgcats.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/vuxml.org.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.bbc.co.uk.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.bdgest.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.bgr.in.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.businessweek.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.cnn.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.developpez.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.egscomics.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.fakingnews.firstpost.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.forbes.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.franceculture.fr.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.futura-sciences.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.geekculture.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.howtogeek.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.lepoint.fr.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.lesnumeriques.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.mac4ever.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.makeuseof.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.monsieur-le-chien.fr.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.npr.org.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.numerama.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.oneindia.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.pcinpact.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.pseudo-sciences.org.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.sciencemag.org.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.slate.fr.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.universfreebox.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.zeit.de.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/xkcd.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Rules/zdnet.com.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Scraper/CandidateParser.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Scraper/ParserInterface.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Scraper/RuleLoader.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Scraper/RuleParser.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Scraper/Scraper.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Serialization/Subscription.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionList.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionListBuilder.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionListParser.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionParser.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Syndication/AtomFeedBuilder.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Syndication/AtomHelper.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Syndication/AtomItemBuilder.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Syndication/FeedBuilder.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Syndication/ItemBuilder.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Syndication/Rss20FeedBuilder.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Syndication/Rss20Helper.php create mode 100644 vendor/fguillot/picofeed/lib/PicoFeed/Syndication/Rss20ItemBuilder.php create mode 100755 vendor/fguillot/picofeed/picofeed create mode 100644 vendor/nikic/fast-route/.travis.yml create mode 100644 vendor/nikic/fast-route/LICENSE create mode 100644 vendor/nikic/fast-route/README.md create mode 100644 vendor/nikic/fast-route/composer.json create mode 100644 vendor/nikic/fast-route/phpunit.xml create mode 100644 vendor/nikic/fast-route/src/BadRouteException.php create mode 100644 vendor/nikic/fast-route/src/DataGenerator.php create mode 100644 vendor/nikic/fast-route/src/DataGenerator/CharCountBased.php create mode 100644 vendor/nikic/fast-route/src/DataGenerator/GroupCountBased.php create mode 100644 vendor/nikic/fast-route/src/DataGenerator/GroupPosBased.php create mode 100644 vendor/nikic/fast-route/src/DataGenerator/MarkBased.php create mode 100644 vendor/nikic/fast-route/src/DataGenerator/RegexBasedAbstract.php create mode 100644 vendor/nikic/fast-route/src/Dispatcher.php create mode 100644 vendor/nikic/fast-route/src/Dispatcher/CharCountBased.php create mode 100644 vendor/nikic/fast-route/src/Dispatcher/GroupCountBased.php create mode 100644 vendor/nikic/fast-route/src/Dispatcher/GroupPosBased.php create mode 100644 vendor/nikic/fast-route/src/Dispatcher/MarkBased.php create mode 100644 vendor/nikic/fast-route/src/Dispatcher/RegexBasedAbstract.php create mode 100644 vendor/nikic/fast-route/src/Route.php create mode 100644 vendor/nikic/fast-route/src/RouteCollector.php create mode 100644 vendor/nikic/fast-route/src/RouteParser.php create mode 100644 vendor/nikic/fast-route/src/RouteParser/Std.php create mode 100644 vendor/nikic/fast-route/src/bootstrap.php create mode 100644 vendor/nikic/fast-route/src/functions.php create mode 100644 vendor/nikic/fast-route/test/Dispatcher/CharCountBasedTest.php create mode 100644 vendor/nikic/fast-route/test/Dispatcher/DispatcherTest.php create mode 100644 vendor/nikic/fast-route/test/Dispatcher/GroupCountBasedTest.php create mode 100644 vendor/nikic/fast-route/test/Dispatcher/GroupPosBasedTest.php create mode 100644 vendor/nikic/fast-route/test/Dispatcher/MarkBasedTest.php create mode 100644 vendor/nikic/fast-route/test/RouteParser/StdTest.php create mode 100644 vendor/nikic/fast-route/test/bootstrap.php create mode 100644 vendor/pimple/pimple/.gitignore create mode 100644 vendor/pimple/pimple/.travis.yml create mode 100644 vendor/pimple/pimple/CHANGELOG create mode 100644 vendor/pimple/pimple/LICENSE create mode 100644 vendor/pimple/pimple/README.rst create mode 100644 vendor/pimple/pimple/composer.json create mode 100644 vendor/pimple/pimple/ext/pimple/.gitignore create mode 100644 vendor/pimple/pimple/ext/pimple/README.md create mode 100644 vendor/pimple/pimple/ext/pimple/config.m4 create mode 100644 vendor/pimple/pimple/ext/pimple/config.w32 create mode 100644 vendor/pimple/pimple/ext/pimple/php_pimple.h create mode 100644 vendor/pimple/pimple/ext/pimple/pimple.c create mode 100644 vendor/pimple/pimple/ext/pimple/pimple_compat.h create mode 100644 vendor/pimple/pimple/ext/pimple/tests/001.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/002.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/003.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/004.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/005.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/006.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/007.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/008.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/009.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/010.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/011.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/012.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/013.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/014.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/015.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/016.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/017.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/017_1.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/018.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/019.phpt create mode 100644 vendor/pimple/pimple/ext/pimple/tests/bench.phpb create mode 100644 vendor/pimple/pimple/ext/pimple/tests/bench_shared.phpb create mode 100644 vendor/pimple/pimple/phpunit.xml.dist create mode 100644 vendor/pimple/pimple/src/Pimple/Container.php create mode 100644 vendor/pimple/pimple/src/Pimple/ServiceProviderInterface.php create mode 100644 vendor/pimple/pimple/src/Pimple/Tests/Fixtures/Invokable.php create mode 100644 vendor/pimple/pimple/src/Pimple/Tests/Fixtures/NonInvokable.php create mode 100644 vendor/pimple/pimple/src/Pimple/Tests/Fixtures/PimpleServiceProvider.php create mode 100644 vendor/pimple/pimple/src/Pimple/Tests/Fixtures/Service.php create mode 100644 vendor/pimple/pimple/src/Pimple/Tests/PimpleServiceProviderInterfaceTest.php create mode 100644 vendor/pimple/pimple/src/Pimple/Tests/PimpleTest.php create mode 100644 vendor/psr/http-message/LICENSE create mode 100644 vendor/psr/http-message/README.md create mode 100644 vendor/psr/http-message/composer.json create mode 100644 vendor/psr/http-message/src/MessageInterface.php create mode 100644 vendor/psr/http-message/src/RequestInterface.php create mode 100644 vendor/psr/http-message/src/ResponseInterface.php create mode 100644 vendor/psr/http-message/src/ServerRequestInterface.php create mode 100644 vendor/psr/http-message/src/StreamInterface.php create mode 100644 vendor/psr/http-message/src/UploadedFileInterface.php create mode 100644 vendor/psr/http-message/src/UriInterface.php create mode 100644 vendor/slim/slim/CONTRIBUTING.md create mode 100644 vendor/slim/slim/LICENSE.md create mode 100644 vendor/slim/slim/README.md create mode 100644 vendor/slim/slim/Slim/App.php create mode 100644 vendor/slim/slim/Slim/CallableResolver.php create mode 100644 vendor/slim/slim/Slim/CallableResolverAwareTrait.php create mode 100644 vendor/slim/slim/Slim/Collection.php create mode 100644 vendor/slim/slim/Slim/Container.php create mode 100644 vendor/slim/slim/Slim/DefaultServicesProvider.php create mode 100644 vendor/slim/slim/Slim/DeferredCallable.php create mode 100644 vendor/slim/slim/Slim/Exception/ContainerValueNotFoundException.php create mode 100644 vendor/slim/slim/Slim/Exception/MethodNotAllowedException.php create mode 100644 vendor/slim/slim/Slim/Exception/NotFoundException.php create mode 100644 vendor/slim/slim/Slim/Exception/SlimException.php create mode 100644 vendor/slim/slim/Slim/Handlers/Error.php create mode 100644 vendor/slim/slim/Slim/Handlers/NotAllowed.php create mode 100644 vendor/slim/slim/Slim/Handlers/NotFound.php create mode 100644 vendor/slim/slim/Slim/Handlers/PhpError.php create mode 100644 vendor/slim/slim/Slim/Handlers/Strategies/RequestResponse.php create mode 100644 vendor/slim/slim/Slim/Handlers/Strategies/RequestResponseArgs.php create mode 100644 vendor/slim/slim/Slim/Http/Body.php create mode 100644 vendor/slim/slim/Slim/Http/Cookies.php create mode 100644 vendor/slim/slim/Slim/Http/Environment.php create mode 100644 vendor/slim/slim/Slim/Http/Headers.php create mode 100644 vendor/slim/slim/Slim/Http/Message.php create mode 100644 vendor/slim/slim/Slim/Http/Request.php create mode 100644 vendor/slim/slim/Slim/Http/RequestBody.php create mode 100644 vendor/slim/slim/Slim/Http/Response.php create mode 100644 vendor/slim/slim/Slim/Http/Stream.php create mode 100644 vendor/slim/slim/Slim/Http/UploadedFile.php create mode 100644 vendor/slim/slim/Slim/Http/Uri.php create mode 100644 vendor/slim/slim/Slim/Interfaces/CallableResolverInterface.php create mode 100644 vendor/slim/slim/Slim/Interfaces/CollectionInterface.php create mode 100644 vendor/slim/slim/Slim/Interfaces/Http/CookiesInterface.php create mode 100644 vendor/slim/slim/Slim/Interfaces/Http/EnvironmentInterface.php create mode 100644 vendor/slim/slim/Slim/Interfaces/Http/HeadersInterface.php create mode 100644 vendor/slim/slim/Slim/Interfaces/InvocationStrategyInterface.php create mode 100644 vendor/slim/slim/Slim/Interfaces/RouteGroupInterface.php create mode 100644 vendor/slim/slim/Slim/Interfaces/RouteInterface.php create mode 100644 vendor/slim/slim/Slim/Interfaces/RouterInterface.php create mode 100644 vendor/slim/slim/Slim/MiddlewareAwareTrait.php create mode 100644 vendor/slim/slim/Slim/Routable.php create mode 100644 vendor/slim/slim/Slim/Route.php create mode 100644 vendor/slim/slim/Slim/RouteGroup.php create mode 100644 vendor/slim/slim/Slim/Router.php create mode 100644 vendor/slim/slim/composer.json create mode 100644 vendor/slim/slim/example/.htaccess create mode 100644 vendor/slim/slim/example/index.php create mode 100644 vendor/symfony/yaml/.gitignore create mode 100644 vendor/symfony/yaml/CHANGELOG.md create mode 100644 vendor/symfony/yaml/Dumper.php create mode 100644 vendor/symfony/yaml/Escaper.php create mode 100644 vendor/symfony/yaml/Exception/DumpException.php create mode 100644 vendor/symfony/yaml/Exception/ExceptionInterface.php create mode 100644 vendor/symfony/yaml/Exception/ParseException.php create mode 100644 vendor/symfony/yaml/Exception/RuntimeException.php create mode 100644 vendor/symfony/yaml/Inline.php create mode 100644 vendor/symfony/yaml/LICENSE create mode 100644 vendor/symfony/yaml/Parser.php create mode 100644 vendor/symfony/yaml/README.md create mode 100644 vendor/symfony/yaml/Tests/DumperTest.php create mode 100644 vendor/symfony/yaml/Tests/Fixtures/YtsAnchorAlias.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/YtsBasicTests.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/YtsBlockMapping.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/YtsDocumentSeparator.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/YtsErrorTests.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/YtsFlowCollections.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/YtsFoldedScalars.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/YtsNullsAndEmpties.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/YtsSpecificationExamples.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/YtsTypeTransfers.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/embededPhp.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/escapedCharacters.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/index.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/sfComments.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/sfCompact.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/sfMergeKey.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/sfObjects.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/sfQuotes.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/sfTests.yml create mode 100644 vendor/symfony/yaml/Tests/Fixtures/unindentedCollections.yml create mode 100644 vendor/symfony/yaml/Tests/InlineTest.php create mode 100644 vendor/symfony/yaml/Tests/ParseExceptionTest.php create mode 100644 vendor/symfony/yaml/Tests/ParserTest.php create mode 100644 vendor/symfony/yaml/Tests/YamlTest.php create mode 100644 vendor/symfony/yaml/Unescaper.php create mode 100644 vendor/symfony/yaml/Yaml.php create mode 100644 vendor/symfony/yaml/composer.json create mode 100644 vendor/symfony/yaml/phpunit.xml.dist create mode 100644 vendor/zendframework/zendxml/.gitignore create mode 100644 vendor/zendframework/zendxml/.travis.yml create mode 100644 vendor/zendframework/zendxml/CHANGELOG.md create mode 100644 vendor/zendframework/zendxml/LICENSE.md create mode 100644 vendor/zendframework/zendxml/README.md create mode 100644 vendor/zendframework/zendxml/composer.json create mode 100644 vendor/zendframework/zendxml/library/ZendXml/Exception/ExceptionInterface.php create mode 100644 vendor/zendframework/zendxml/library/ZendXml/Exception/InvalidArgumentException.php create mode 100644 vendor/zendframework/zendxml/library/ZendXml/Exception/RuntimeException.php create mode 100644 vendor/zendframework/zendxml/library/ZendXml/Security.php create mode 100644 vendor/zendframework/zendxml/tests/Bootstrap.php create mode 100644 vendor/zendframework/zendxml/tests/ZendXmlTest/MultibyteTest.php create mode 100644 vendor/zendframework/zendxml/tests/ZendXmlTest/SecurityTest.php create mode 100755 vendor/zendframework/zendxml/tests/phpunit.xml.dist diff --git a/.gitignore b/.gitignore index b03df61..3189517 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ nbproject/ .idea/ completer.hist feed/ -vendor/ node_modules/ diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000..b1be175 --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,7 @@ +decode() + * bug #279 predis wrong number of arguments + * bug #277 Securing JS WebApp client secret w/ password grant type + +* 1.1 (2013-12-17) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/276 + + * bug #278 adds refresh token configuration to Server class + * bug #274 Supplying a null client_id and client_secret grants API access + * bug #244 [MongoStorage] More detailed implementation info + * bug #268 Implement jti for JWT Bearer tokens to prevent replay attacks. + * bug #266 Removing unused argument to getAccessTokenData + * bug #247 Make Bearer token type consistent + * bug #253 Fixing CryptoToken refresh token lifetime + * bug #246 refactors public key logic to be more intuitive + * bug #245 adds support for JSON crypto tokens + * bug #230 Remove unused columns in oauth_clients + * bug #215 makes Redis Scope Storage obey the same paradigm as PDO + * bug #228 removes scope group + * bug #227 squelches open basedir restriction error + * bug #223 Updated docblocks for RefreshTokenInterface.php + * bug #224 Adds protected properties + * bug #217 Implement ScopeInterface for PDO, Redis + +* 1.0 (2013-08-12) + + * bug #203 Add redirect\_status_code config param for AuthorizeController + * bug #205 ensures unnecessary ? is not set when ** bug + * bug #204 Fixed call to LogicException + * bug #202 Add explode to checkRestrictedGrant in PDO Storage + * bug #197 adds support for 'false' default scope ** bug + * bug #192 reference errors and adds tests + * bug #194 makes some appropriate properties ** bug + * bug #191 passes config to HttpBasic + * bug #190 validates client credentials before ** bug + * bug #171 Fix wrong redirect following authorization step + * bug #187 client_id is now passed to getDefaultScope(). + * bug #176 Require refresh_token in getRefreshToken response + * bug #174 make user\_id not required for refresh_token grant + * bug #173 Duplication in JwtBearer Grant + * bug #168 user\_id not required for authorization_code grant + * bug #133 hardens default security for user object + * bug #163 allows redirect\_uri on authorization_code to be NULL in docs example + * bug #162 adds getToken on ResourceController for convenience + * bug #161 fixes fatal error + * bug #163 Invalid redirect_uri handling + * bug #156 user\_id in OAuth2\_Storage_AuthorizationCodeInterface::getAuthorizationCode() response + * bug #157 Fix for extending access and refresh tokens + * bug #154 ResponseInterface: getParameter method is used in the library but not defined in the interface + * bug #148 Add more detail to examples in Readme.md diff --git a/vendor/bshaffer/oauth2-server-php/LICENSE b/vendor/bshaffer/oauth2-server-php/LICENSE new file mode 100644 index 0000000..d7ece84 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2014 Brent Shaffer + +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: + +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/vendor/bshaffer/oauth2-server-php/README.md b/vendor/bshaffer/oauth2-server-php/README.md new file mode 100644 index 0000000..4ceda6c --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/README.md @@ -0,0 +1,8 @@ +oauth2-server-php +================= + +[![Build Status](https://travis-ci.org/bshaffer/oauth2-server-php.svg?branch=develop)](https://travis-ci.org/bshaffer/oauth2-server-php) + +[![Total Downloads](https://poser.pugx.org/bshaffer/oauth2-server-php/downloads.png)](https://packagist.org/packages/bshaffer/oauth2-server-php) + +View the [complete documentation](http://bshaffer.github.io/oauth2-server-php-docs/) \ No newline at end of file diff --git a/vendor/bshaffer/oauth2-server-php/composer.json b/vendor/bshaffer/oauth2-server-php/composer.json new file mode 100644 index 0000000..853c041 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/composer.json @@ -0,0 +1,27 @@ +{ + "name": "bshaffer/oauth2-server-php", + "description":"OAuth2 Server for PHP", + "keywords":["oauth","oauth2","auth"], + "type":"library", + "license":"MIT", + "authors":[ + { + "name":"Brent Shaffer", + "email": "bshafs@gmail.com", + "homepage":"http://brentertainment.com" + } + ], + "homepage": "http://github.com/bshaffer/oauth2-server-php", + "require":{ + "php":">=5.3.9" + }, + "autoload": { + "psr-0": { "OAuth2": "src/" } + }, + "suggest": { + "predis/predis": "Required to use the Redis storage engine", + "thobbs/phpcassa": "Required to use the Cassandra storage engine", + "aws/aws-sdk-php": "~2.8 is required to use the DynamoDB storage engine", + "firebase/php-jwt": "~2.2 is required to use JWT features" + } +} diff --git a/vendor/bshaffer/oauth2-server-php/phpunit.xml b/vendor/bshaffer/oauth2-server-php/phpunit.xml new file mode 100644 index 0000000..e36403f --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/phpunit.xml @@ -0,0 +1,25 @@ + + + + + + ./test/OAuth2/ + + + + + + ./src/OAuth2/ + + + diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Autoloader.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Autoloader.php new file mode 100644 index 0000000..ecfb6ba --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Autoloader.php @@ -0,0 +1,48 @@ + + * @license MIT License + */ +class Autoloader +{ + private $dir; + + public function __construct($dir = null) + { + if (is_null($dir)) { + $dir = dirname(__FILE__).'/..'; + } + $this->dir = $dir; + } + /** + * Registers OAuth2\Autoloader as an SPL autoloader. + */ + public static function register($dir = null) + { + ini_set('unserialize_callback_func', 'spl_autoload_call'); + spl_autoload_register(array(new self($dir), 'autoload')); + } + + /** + * Handles autoloading of classes. + * + * @param string $class A class name. + * + * @return boolean Returns true if the class has been loaded + */ + public function autoload($class) + { + if (0 !== strpos($class, 'OAuth2')) { + return; + } + + if (file_exists($file = $this->dir.'/'.str_replace('\\', '/', $class).'.php')) { + require $file; + } + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php new file mode 100644 index 0000000..29c7171 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php @@ -0,0 +1,15 @@ + + */ +class HttpBasic implements ClientAssertionTypeInterface +{ + private $clientData; + + protected $storage; + protected $config; + + /** + * @param OAuth2\Storage\ClientCredentialsInterface $clientStorage REQUIRED Storage class for retrieving client credentials information + * @param array $config OPTIONAL Configuration options for the server + * + * $config = array( + * 'allow_credentials_in_request_body' => true, // whether to look for credentials in the POST body in addition to the Authorize HTTP Header + * 'allow_public_clients' => true // if true, "public clients" (clients without a secret) may be authenticated + * ); + * + */ + public function __construct(ClientCredentialsInterface $storage, array $config = array()) + { + $this->storage = $storage; + $this->config = array_merge(array( + 'allow_credentials_in_request_body' => true, + 'allow_public_clients' => true, + ), $config); + } + + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$clientData = $this->getClientCredentials($request, $response)) { + return false; + } + + if (!isset($clientData['client_id'])) { + throw new \LogicException('the clientData array must have "client_id" set'); + } + + if (!isset($clientData['client_secret']) || $clientData['client_secret'] == '') { + if (!$this->config['allow_public_clients']) { + $response->setError(400, 'invalid_client', 'client credentials are required'); + + return false; + } + + if (!$this->storage->isPublicClient($clientData['client_id'])) { + $response->setError(400, 'invalid_client', 'This client is invalid or must authenticate using a client secret'); + + return false; + } + } elseif ($this->storage->checkClientCredentials($clientData['client_id'], $clientData['client_secret']) === false) { + $response->setError(400, 'invalid_client', 'The client credentials are invalid'); + + return false; + } + + $this->clientData = $clientData; + + return true; + } + + public function getClientId() + { + return $this->clientData['client_id']; + } + + /** + * Internal function used to get the client credentials from HTTP basic + * auth or POST data. + * + * According to the spec (draft 20), the client_id can be provided in + * the Basic Authorization header (recommended) or via GET/POST. + * + * @return + * A list containing the client identifier and password, for example + * @code + * return array( + * "client_id" => CLIENT_ID, // REQUIRED the client id + * "client_secret" => CLIENT_SECRET, // OPTIONAL the client secret (may be omitted for public clients) + * ); + * @endcode + * + * @see http://tools.ietf.org/html/rfc6749#section-2.3.1 + * + * @ingroup oauth2_section_2 + */ + public function getClientCredentials(RequestInterface $request, ResponseInterface $response = null) + { + if (!is_null($request->headers('PHP_AUTH_USER')) && !is_null($request->headers('PHP_AUTH_PW'))) { + return array('client_id' => $request->headers('PHP_AUTH_USER'), 'client_secret' => $request->headers('PHP_AUTH_PW')); + } + + if ($this->config['allow_credentials_in_request_body']) { + // Using POST for HttpBasic authorization is not recommended, but is supported by specification + if (!is_null($request->request('client_id'))) { + /** + * client_secret can be null if the client's password is an empty string + * @see http://tools.ietf.org/html/rfc6749#section-2.3.1 + */ + + return array('client_id' => $request->request('client_id'), 'client_secret' => $request->request('client_secret')); + } + } + + if ($response) { + $message = $this->config['allow_credentials_in_request_body'] ? ' or body' : ''; + $response->setError(400, 'invalid_client', 'Client credentials were not found in the headers'.$message); + } + + return null; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeController.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeController.php new file mode 100644 index 0000000..91c275c --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeController.php @@ -0,0 +1,383 @@ + + * $config = array( + * 'allow_implicit' => false, // if the controller should allow the "implicit" grant type + * 'enforce_state' => true // if the controller should require the "state" parameter + * 'require_exact_redirect_uri' => true, // if the controller should require an exact match on the "redirect_uri" parameter + * 'redirect_status_code' => 302, // HTTP status code to use for redirect responses + * ); + * + * @param OAuth2\ScopeInterface $scopeUtil OPTIONAL Instance of OAuth2\ScopeInterface to validate the requested scope + */ + public function __construct(ClientInterface $clientStorage, array $responseTypes = array(), array $config = array(), ScopeInterface $scopeUtil = null) + { + $this->clientStorage = $clientStorage; + $this->responseTypes = $responseTypes; + $this->config = array_merge(array( + 'allow_implicit' => false, + 'enforce_state' => true, + 'require_exact_redirect_uri' => true, + 'redirect_status_code' => 302, + ), $config); + + if (is_null($scopeUtil)) { + $scopeUtil = new Scope(); + } + $this->scopeUtil = $scopeUtil; + } + + public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null) + { + if (!is_bool($is_authorized)) { + throw new \InvalidArgumentException('Argument "is_authorized" must be a boolean. This method must know if the user has granted access to the client.'); + } + + // We repeat this, because we need to re-validate. The request could be POSTed + // by a 3rd-party (because we are not internally enforcing NONCEs, etc) + if (!$this->validateAuthorizeRequest($request, $response)) { + return; + } + + // If no redirect_uri is passed in the request, use client's registered one + if (empty($this->redirect_uri)) { + $clientData = $this->clientStorage->getClientDetails($this->client_id); + $registered_redirect_uri = $clientData['redirect_uri']; + } + + // the user declined access to the client's application + if ($is_authorized === false) { + $redirect_uri = $this->redirect_uri ?: $registered_redirect_uri; + $this->setNotAuthorizedResponse($request, $response, $redirect_uri, $user_id); + + return; + } + + // build the parameters to set in the redirect URI + if (!$params = $this->buildAuthorizeParameters($request, $response, $user_id)) { + return; + } + + $authResult = $this->responseTypes[$this->response_type]->getAuthorizeResponse($params, $user_id); + + list($redirect_uri, $uri_params) = $authResult; + + if (empty($redirect_uri) && !empty($registered_redirect_uri)) { + $redirect_uri = $registered_redirect_uri; + } + + $uri = $this->buildUri($redirect_uri, $uri_params); + + // return redirect response + $response->setRedirect($this->config['redirect_status_code'], $uri); + } + + protected function setNotAuthorizedResponse(RequestInterface $request, ResponseInterface $response, $redirect_uri, $user_id = null) + { + $error = 'access_denied'; + $error_message = 'The user denied access to your application'; + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->state, $error, $error_message); + } + + /* + * We have made this protected so this class can be extended to add/modify + * these parameters + */ + protected function buildAuthorizeParameters($request, $response, $user_id) + { + // @TODO: we should be explicit with this in the future + $params = array( + 'scope' => $this->scope, + 'state' => $this->state, + 'client_id' => $this->client_id, + 'redirect_uri' => $this->redirect_uri, + 'response_type' => $this->response_type, + ); + + return $params; + } + + public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response) + { + // Make sure a valid client id was supplied (we can not redirect because we were unable to verify the URI) + if (!$client_id = $request->query('client_id', $request->request('client_id'))) { + // We don't have a good URI to use + $response->setError(400, 'invalid_client', "No client id supplied"); + + return false; + } + + // Get client details + if (!$clientData = $this->clientStorage->getClientDetails($client_id)) { + $response->setError(400, 'invalid_client', 'The client id supplied is invalid'); + + return false; + } + + $registered_redirect_uri = isset($clientData['redirect_uri']) ? $clientData['redirect_uri'] : ''; + + // Make sure a valid redirect_uri was supplied. If specified, it must match the clientData URI. + // @see http://tools.ietf.org/html/rfc6749#section-3.1.2 + // @see http://tools.ietf.org/html/rfc6749#section-4.1.2.1 + // @see http://tools.ietf.org/html/rfc6749#section-4.2.2.1 + if ($supplied_redirect_uri = $request->query('redirect_uri', $request->request('redirect_uri'))) { + // validate there is no fragment supplied + $parts = parse_url($supplied_redirect_uri); + if (isset($parts['fragment']) && $parts['fragment']) { + $response->setError(400, 'invalid_uri', 'The redirect URI must not contain a fragment'); + + return false; + } + + // validate against the registered redirect uri(s) if available + if ($registered_redirect_uri && !$this->validateRedirectUri($supplied_redirect_uri, $registered_redirect_uri)) { + $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI provided is missing or does not match', '#section-3.1.2'); + + return false; + } + $redirect_uri = $supplied_redirect_uri; + } else { + // use the registered redirect_uri if none has been supplied, if possible + if (!$registered_redirect_uri) { + $response->setError(400, 'invalid_uri', 'No redirect URI was supplied or stored'); + + return false; + } + + if (count(explode(' ', $registered_redirect_uri)) > 1) { + $response->setError(400, 'invalid_uri', 'A redirect URI must be supplied when multiple redirect URIs are registered', '#section-3.1.2.3'); + + return false; + } + $redirect_uri = $registered_redirect_uri; + } + + // Select the redirect URI + $response_type = $request->query('response_type', $request->request('response_type')); + + // for multiple-valued response types - make them alphabetical + if (false !== strpos($response_type, ' ')) { + $types = explode(' ', $response_type); + sort($types); + $response_type = ltrim(implode(' ', $types)); + } + + $state = $request->query('state', $request->request('state')); + + // type and client_id are required + if (!$response_type || !in_array($response_type, $this->getValidResponseTypes())) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_request', 'Invalid or missing response type', null); + + return false; + } + + if ($response_type == self::RESPONSE_TYPE_AUTHORIZATION_CODE) { + if (!isset($this->responseTypes['code'])) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'authorization code grant type not supported', null); + + return false; + } + if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'authorization_code')) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null); + + return false; + } + if ($this->responseTypes['code']->enforceRedirect() && !$redirect_uri) { + $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI is mandatory and was not supplied'); + + return false; + } + } else { + if (!$this->config['allow_implicit']) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'implicit grant type not supported', null); + + return false; + } + if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'implicit')) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null); + + return false; + } + } + + // validate requested scope if it exists + $requestedScope = $this->scopeUtil->getScopeFromRequest($request); + + if ($requestedScope) { + // restrict scope by client specific scope if applicable, + // otherwise verify the scope exists + $clientScope = $this->clientStorage->getClientScope($client_id); + if ((is_null($clientScope) && !$this->scopeUtil->scopeExists($requestedScope)) + || ($clientScope && !$this->scopeUtil->checkScope($requestedScope, $clientScope))) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_scope', 'An unsupported scope was requested', null); + + return false; + } + } else { + // use a globally-defined default scope + $defaultScope = $this->scopeUtil->getDefaultScope($client_id); + + if (false === $defaultScope) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_client', 'This application requires you specify a scope parameter', null); + + return false; + } + + $requestedScope = $defaultScope; + } + + // Validate state parameter exists (if configured to enforce this) + if ($this->config['enforce_state'] && !$state) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, null, 'invalid_request', 'The state parameter is required'); + + return false; + } + + // save the input data and return true + $this->scope = $requestedScope; + $this->state = $state; + $this->client_id = $client_id; + // Only save the SUPPLIED redirect URI (@see http://tools.ietf.org/html/rfc6749#section-4.1.3) + $this->redirect_uri = $supplied_redirect_uri; + $this->response_type = $response_type; + + return true; + } + + /** + * Build the absolute URI based on supplied URI and parameters. + * + * @param $uri An absolute URI. + * @param $params Parameters to be append as GET. + * + * @return + * An absolute URI with supplied parameters. + * + * @ingroup oauth2_section_4 + */ + private function buildUri($uri, $params) + { + $parse_url = parse_url($uri); + + // Add our params to the parsed uri + foreach ($params as $k => $v) { + if (isset($parse_url[$k])) { + $parse_url[$k] .= "&" . http_build_query($v, '', '&'); + } else { + $parse_url[$k] = http_build_query($v, '', '&'); + } + } + + // Put humpty dumpty back together + return + ((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "") + . ((isset($parse_url["user"])) ? $parse_url["user"] + . ((isset($parse_url["pass"])) ? ":" . $parse_url["pass"] : "") . "@" : "") + . ((isset($parse_url["host"])) ? $parse_url["host"] : "") + . ((isset($parse_url["port"])) ? ":" . $parse_url["port"] : "") + . ((isset($parse_url["path"])) ? $parse_url["path"] : "") + . ((isset($parse_url["query"]) && !empty($parse_url['query'])) ? "?" . $parse_url["query"] : "") + . ((isset($parse_url["fragment"])) ? "#" . $parse_url["fragment"] : "") + ; + } + + protected function getValidResponseTypes() + { + return array( + self::RESPONSE_TYPE_ACCESS_TOKEN, + self::RESPONSE_TYPE_AUTHORIZATION_CODE, + ); + } + + /** + * Internal method for validating redirect URI supplied + * + * @param string $inputUri The submitted URI to be validated + * @param string $registeredUriString The allowed URI(s) to validate against. Can be a space-delimited string of URIs to + * allow for multiple URIs + * + * @see http://tools.ietf.org/html/rfc6749#section-3.1.2 + */ + protected function validateRedirectUri($inputUri, $registeredUriString) + { + if (!$inputUri || !$registeredUriString) { + return false; // if either one is missing, assume INVALID + } + + $registered_uris = preg_split('/\s+/', $registeredUriString); + foreach ($registered_uris as $registered_uri) { + if ($this->config['require_exact_redirect_uri']) { + // the input uri is validated against the registered uri using exact match + if (strcmp($inputUri, $registered_uri) === 0) { + return true; + } + } else { + // the input uri is validated against the registered uri using case-insensitive match of the initial string + // i.e. additional query parameters may be applied + if (strcasecmp(substr($inputUri, 0, strlen($registered_uri)), $registered_uri) === 0) { + return true; + } + } + } + + return false; + } + + /** + * Convenience methods to access the parameters derived from the validated request + */ + + public function getScope() + { + return $this->scope; + } + + public function getState() + { + return $this->state; + } + + public function getClientId() + { + return $this->client_id; + } + + public function getRedirectUri() + { + return $this->redirect_uri; + } + + public function getResponseType() + { + return $this->response_type; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeControllerInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeControllerInterface.php new file mode 100644 index 0000000..fa07ae8 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeControllerInterface.php @@ -0,0 +1,43 @@ + $user_id = $this->somehowDetermineUserId(); + * > $is_authorized = $this->somehowDetermineUserAuthorization(); + * > $response = new OAuth2\Response(); + * > $authorizeController->handleAuthorizeRequest( + * > OAuth2\Request::createFromGlobals(), + * > $response, + * > $is_authorized, + * > $user_id); + * > $response->send(); + * + */ +interface AuthorizeControllerInterface +{ + /** + * List of possible authentication response types. + * The "authorization_code" mechanism exclusively supports 'code' + * and the "implicit" mechanism exclusively supports 'token'. + * + * @var string + * @see http://tools.ietf.org/html/rfc6749#section-4.1.1 + * @see http://tools.ietf.org/html/rfc6749#section-4.2.1 + */ + const RESPONSE_TYPE_AUTHORIZATION_CODE = 'code'; + const RESPONSE_TYPE_ACCESS_TOKEN = 'token'; + + public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null); + + public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceController.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceController.php new file mode 100644 index 0000000..e858818 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceController.php @@ -0,0 +1,111 @@ +tokenType = $tokenType; + $this->tokenStorage = $tokenStorage; + + $this->config = array_merge(array( + 'www_realm' => 'Service', + ), $config); + + if (is_null($scopeUtil)) { + $scopeUtil = new Scope(); + } + $this->scopeUtil = $scopeUtil; + } + + public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null) + { + $token = $this->getAccessTokenData($request, $response); + + // Check if we have token data + if (is_null($token)) { + return false; + } + + /** + * Check scope, if provided + * If token doesn't have a scope, it's null/empty, or it's insufficient, then throw 403 + * @see http://tools.ietf.org/html/rfc6750#section-3.1 + */ + if ($scope && (!isset($token["scope"]) || !$token["scope"] || !$this->scopeUtil->checkScope($scope, $token["scope"]))) { + $response->setError(403, 'insufficient_scope', 'The request requires higher privileges than provided by the access token'); + $response->addHttpHeaders(array( + 'WWW-Authenticate' => sprintf('%s realm="%s", scope="%s", error="%s", error_description="%s"', + $this->tokenType->getTokenType(), + $this->config['www_realm'], + $scope, + $response->getParameter('error'), + $response->getParameter('error_description') + ) + )); + + return false; + } + + // allow retrieval of the token + $this->token = $token; + + return (bool) $token; + } + + public function getAccessTokenData(RequestInterface $request, ResponseInterface $response) + { + // Get the token parameter + if ($token_param = $this->tokenType->getAccessTokenParameter($request, $response)) { + // Get the stored token data (from the implementing subclass) + // Check we have a well formed token + // Check token expiration (expires is a mandatory paramter) + if (!$token = $this->tokenStorage->getAccessToken($token_param)) { + $response->setError(401, 'invalid_token', 'The access token provided is invalid'); + } elseif (!isset($token["expires"]) || !isset($token["client_id"])) { + $response->setError(401, 'malformed_token', 'Malformed token (missing "expires")'); + } elseif (time() > $token["expires"]) { + $response->setError(401, 'expired_token', 'The access token provided has expired'); + } else { + return $token; + } + } + + $authHeader = sprintf('%s realm="%s"', $this->tokenType->getTokenType(), $this->config['www_realm']); + + if ($error = $response->getParameter('error')) { + $authHeader = sprintf('%s, error="%s"', $authHeader, $error); + if ($error_description = $response->getParameter('error_description')) { + $authHeader = sprintf('%s, error_description="%s"', $authHeader, $error_description); + } + } + + $response->addHttpHeaders(array('WWW-Authenticate' => $authHeader)); + + return null; + } + + // convenience method to allow retrieval of the token + public function getToken() + { + return $this->token; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceControllerInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceControllerInterface.php new file mode 100644 index 0000000..6114219 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceControllerInterface.php @@ -0,0 +1,26 @@ + if (!$resourceController->verifyResourceRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response())) { + * > $response->send(); // authorization failed + * > die(); + * > } + * > return json_encode($resource); // valid token! Send the stuff! + * + */ +interface ResourceControllerInterface +{ + public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null); + + public function getAccessTokenData(RequestInterface $request, ResponseInterface $response); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenController.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenController.php new file mode 100644 index 0000000..2e0750f --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenController.php @@ -0,0 +1,274 @@ +clientAssertionType = $clientAssertionType; + $this->accessToken = $accessToken; + $this->clientStorage = $clientStorage; + foreach ($grantTypes as $grantType) { + $this->addGrantType($grantType); + } + + if (is_null($scopeUtil)) { + $scopeUtil = new Scope(); + } + $this->scopeUtil = $scopeUtil; + } + + public function handleTokenRequest(RequestInterface $request, ResponseInterface $response) + { + if ($token = $this->grantAccessToken($request, $response)) { + // @see http://tools.ietf.org/html/rfc6749#section-5.1 + // server MUST disable caching in headers when tokens are involved + $response->setStatusCode(200); + $response->addParameters($token); + $response->addHttpHeaders(array('Cache-Control' => 'no-store', 'Pragma' => 'no-cache')); + } + } + + /** + * Grant or deny a requested access token. + * This would be called from the "/token" endpoint as defined in the spec. + * You can call your endpoint whatever you want. + * + * @param $request - RequestInterface + * Request object to grant access token + * + * @throws InvalidArgumentException + * @throws LogicException + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @see http://tools.ietf.org/html/rfc6749#section-10.6 + * @see http://tools.ietf.org/html/rfc6749#section-4.1.3 + * + * @ingroup oauth2_section_4 + */ + public function grantAccessToken(RequestInterface $request, ResponseInterface $response) + { + if (strtolower($request->server('REQUEST_METHOD')) != 'post') { + $response->setError(405, 'invalid_request', 'The request method must be POST when requesting an access token', '#section-3.2'); + $response->addHttpHeaders(array('Allow' => 'POST')); + + return null; + } + + /** + * Determine grant type from request + * and validate the request for that grant type + */ + if (!$grantTypeIdentifier = $request->request('grant_type')) { + $response->setError(400, 'invalid_request', 'The grant type was not specified in the request'); + + return null; + } + + if (!isset($this->grantTypes[$grantTypeIdentifier])) { + /* TODO: If this is an OAuth2 supported grant type that we have chosen not to implement, throw a 501 Not Implemented instead */ + $response->setError(400, 'unsupported_grant_type', sprintf('Grant type "%s" not supported', $grantTypeIdentifier)); + + return null; + } + + $grantType = $this->grantTypes[$grantTypeIdentifier]; + + /** + * Retrieve the client information from the request + * ClientAssertionTypes allow for grant types which also assert the client data + * in which case ClientAssertion is handled in the validateRequest method + * + * @see OAuth2\GrantType\JWTBearer + * @see OAuth2\GrantType\ClientCredentials + */ + if (!$grantType instanceof ClientAssertionTypeInterface) { + if (!$this->clientAssertionType->validateRequest($request, $response)) { + return null; + } + $clientId = $this->clientAssertionType->getClientId(); + } + + /** + * Retrieve the grant type information from the request + * The GrantTypeInterface object handles all validation + * If the object is an instance of ClientAssertionTypeInterface, + * That logic is handled here as well + */ + if (!$grantType->validateRequest($request, $response)) { + return null; + } + + if ($grantType instanceof ClientAssertionTypeInterface) { + $clientId = $grantType->getClientId(); + } else { + // validate the Client ID (if applicable) + if (!is_null($storedClientId = $grantType->getClientId()) && $storedClientId != $clientId) { + $response->setError(400, 'invalid_grant', sprintf('%s doesn\'t exist or is invalid for the client', $grantTypeIdentifier)); + + return null; + } + } + + /** + * Validate the client can use the requested grant type + */ + if (!$this->clientStorage->checkRestrictedGrantType($clientId, $grantTypeIdentifier)) { + $response->setError(400, 'unauthorized_client', 'The grant type is unauthorized for this client_id'); + + return false; + } + + /** + * Validate the scope of the token + * + * requestedScope - the scope specified in the token request + * availableScope - the scope associated with the grant type + * ex: in the case of the "Authorization Code" grant type, + * the scope is specified in the authorize request + * + * @see http://tools.ietf.org/html/rfc6749#section-3.3 + */ + + $requestedScope = $this->scopeUtil->getScopeFromRequest($request); + $availableScope = $grantType->getScope(); + + if ($requestedScope) { + // validate the requested scope + if ($availableScope) { + if (!$this->scopeUtil->checkScope($requestedScope, $availableScope)) { + $response->setError(400, 'invalid_scope', 'The scope requested is invalid for this request'); + + return null; + } + } else { + // validate the client has access to this scope + if ($clientScope = $this->clientStorage->getClientScope($clientId)) { + if (!$this->scopeUtil->checkScope($requestedScope, $clientScope)) { + $response->setError(400, 'invalid_scope', 'The scope requested is invalid for this client'); + + return false; + } + } elseif (!$this->scopeUtil->scopeExists($requestedScope)) { + $response->setError(400, 'invalid_scope', 'An unsupported scope was requested'); + + return null; + } + } + } elseif ($availableScope) { + // use the scope associated with this grant type + $requestedScope = $availableScope; + } else { + // use a globally-defined default scope + $defaultScope = $this->scopeUtil->getDefaultScope($clientId); + + // "false" means default scopes are not allowed + if (false === $defaultScope) { + $response->setError(400, 'invalid_scope', 'This application requires you specify a scope parameter'); + + return null; + } + + $requestedScope = $defaultScope; + } + + return $grantType->createAccessToken($this->accessToken, $clientId, $grantType->getUserId(), $requestedScope); + } + + /** + * addGrantType + * + * @param grantType - OAuth2\GrantTypeInterface + * the grant type to add for the specified identifier + * @param identifier - string + * a string passed in as "grant_type" in the response that will call this grantType + */ + public function addGrantType(GrantTypeInterface $grantType, $identifier = null) + { + if (is_null($identifier) || is_numeric($identifier)) { + $identifier = $grantType->getQuerystringIdentifier(); + } + + $this->grantTypes[$identifier] = $grantType; + } + + public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response) + { + if ($this->revokeToken($request, $response)) { + $response->setStatusCode(200); + $response->addParameters(array('revoked' => true)); + } + } + + /** + * Revoke a refresh or access token. Returns true on success and when tokens are invalid + * + * Note: invalid tokens do not cause an error response since the client + * cannot handle such an error in a reasonable way. Moreover, the + * purpose of the revocation request, invalidating the particular token, + * is already achieved. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool|null + */ + public function revokeToken(RequestInterface $request, ResponseInterface $response) + { + if (strtolower($request->server('REQUEST_METHOD')) != 'post') { + $response->setError(405, 'invalid_request', 'The request method must be POST when revoking an access token', '#section-3.2'); + $response->addHttpHeaders(array('Allow' => 'POST')); + + return null; + } + + $token_type_hint = $request->request('token_type_hint'); + if (!in_array($token_type_hint, array(null, 'access_token', 'refresh_token'), true)) { + $response->setError(400, 'invalid_request', 'Token type hint must be either \'access_token\' or \'refresh_token\''); + + return null; + } + + $token = $request->request('token'); + if ($token === null) { + $response->setError(400, 'invalid_request', 'Missing token parameter to revoke'); + + return null; + } + + // @todo remove this check for v2.0 + if (!method_exists($this->accessToken, 'revokeToken')) { + $class = get_class($this->accessToken); + throw new \RuntimeException("AccessToken {$class} does not implement required revokeToken method"); + } + + $this->accessToken->revokeToken($token, $token_type_hint); + + return true; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenControllerInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenControllerInterface.php new file mode 100644 index 0000000..72d7257 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenControllerInterface.php @@ -0,0 +1,32 @@ + $tokenController->handleTokenRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response()); + * > $response->send(); + * + */ +interface TokenControllerInterface +{ + /** + * handleTokenRequest + * + * @param $request + * OAuth2\RequestInterface - The current http request + * @param $response + * OAuth2\ResponseInterface - An instance of OAuth2\ResponseInterface to contain the response data + * + */ + public function handleTokenRequest(RequestInterface $request, ResponseInterface $response); + + public function grantAccessToken(RequestInterface $request, ResponseInterface $response); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/EncryptionInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/EncryptionInterface.php new file mode 100644 index 0000000..2d336c6 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/EncryptionInterface.php @@ -0,0 +1,11 @@ + + */ +class FirebaseJwt implements EncryptionInterface +{ + public function __construct() + { + if (!class_exists('\JWT')) { + throw new \ErrorException('firebase/php-jwt must be installed to use this feature. You can do this by running "composer require firebase/php-jwt"'); + } + } + + public function encode($payload, $key, $alg = 'HS256', $keyId = null) + { + return \JWT::encode($payload, $key, $alg, $keyId); + } + + public function decode($jwt, $key = null, $allowedAlgorithms = null) + { + try { + + //Maintain BC: Do not verify if no algorithms are passed in. + if (!$allowedAlgorithms) { + $key = null; + } + + return (array)\JWT::decode($jwt, $key, $allowedAlgorithms); + } catch (\Exception $e) { + return false; + } + } + + public function urlSafeB64Encode($data) + { + return \JWT::urlsafeB64Encode($data); + } + + public function urlSafeB64Decode($b64) + { + return \JWT::urlsafeB64Decode($b64); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/Jwt.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/Jwt.php new file mode 100644 index 0000000..ee576e6 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/Jwt.php @@ -0,0 +1,173 @@ +generateJwtHeader($payload, $algo); + + $segments = array( + $this->urlSafeB64Encode(json_encode($header)), + $this->urlSafeB64Encode(json_encode($payload)) + ); + + $signing_input = implode('.', $segments); + + $signature = $this->sign($signing_input, $key, $algo); + $segments[] = $this->urlsafeB64Encode($signature); + + return implode('.', $segments); + } + + public function decode($jwt, $key = null, $allowedAlgorithms = true) + { + if (!strpos($jwt, '.')) { + return false; + } + + $tks = explode('.', $jwt); + + if (count($tks) != 3) { + return false; + } + + list($headb64, $payloadb64, $cryptob64) = $tks; + + if (null === ($header = json_decode($this->urlSafeB64Decode($headb64), true))) { + return false; + } + + if (null === $payload = json_decode($this->urlSafeB64Decode($payloadb64), true)) { + return false; + } + + $sig = $this->urlSafeB64Decode($cryptob64); + + if ((bool) $allowedAlgorithms) { + if (!isset($header['alg'])) { + return false; + } + + // check if bool arg supplied here to maintain BC + if (is_array($allowedAlgorithms) && !in_array($header['alg'], $allowedAlgorithms)) { + return false; + } + + if (!$this->verifySignature($sig, "$headb64.$payloadb64", $key, $header['alg'])) { + return false; + } + } + + return $payload; + } + + private function verifySignature($signature, $input, $key, $algo = 'HS256') + { + // use constants when possible, for HipHop support + switch ($algo) { + case'HS256': + case'HS384': + case'HS512': + return $this->hash_equals( + $this->sign($input, $key, $algo), + $signature + ); + + case 'RS256': + return openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256') === 1; + + case 'RS384': + return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384') === 1; + + case 'RS512': + return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512') === 1; + + default: + throw new \InvalidArgumentException("Unsupported or invalid signing algorithm."); + } + } + + private function sign($input, $key, $algo = 'HS256') + { + switch ($algo) { + case 'HS256': + return hash_hmac('sha256', $input, $key, true); + + case 'HS384': + return hash_hmac('sha384', $input, $key, true); + + case 'HS512': + return hash_hmac('sha512', $input, $key, true); + + case 'RS256': + return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256'); + + case 'RS384': + return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384'); + + case 'RS512': + return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512'); + + default: + throw new \Exception("Unsupported or invalid signing algorithm."); + } + } + + private function generateRSASignature($input, $key, $algo) + { + if (!openssl_sign($input, $signature, $key, $algo)) { + throw new \Exception("Unable to sign data."); + } + + return $signature; + } + + public function urlSafeB64Encode($data) + { + $b64 = base64_encode($data); + $b64 = str_replace(array('+', '/', "\r", "\n", '='), + array('-', '_'), + $b64); + + return $b64; + } + + public function urlSafeB64Decode($b64) + { + $b64 = str_replace(array('-', '_'), + array('+', '/'), + $b64); + + return base64_decode($b64); + } + + /** + * Override to create a custom header + */ + protected function generateJwtHeader($payload, $algorithm) + { + return array( + 'typ' => 'JWT', + 'alg' => $algorithm, + ); + } + + protected function hash_equals($a, $b) + { + if (function_exists('hash_equals')) { + return hash_equals($a, $b); + } + $diff = strlen($a) ^ strlen($b); + for ($i = 0; $i < strlen($a) && $i < strlen($b); $i++) { + $diff |= ord($a[$i]) ^ ord($b[$i]); + } + + return $diff === 0; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/AuthorizationCode.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/AuthorizationCode.php new file mode 100644 index 0000000..e899520 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/AuthorizationCode.php @@ -0,0 +1,100 @@ + + */ +class AuthorizationCode implements GrantTypeInterface +{ + protected $storage; + protected $authCode; + + /** + * @param OAuth2\Storage\AuthorizationCodeInterface $storage REQUIRED Storage class for retrieving authorization code information + */ + public function __construct(AuthorizationCodeInterface $storage) + { + $this->storage = $storage; + } + + public function getQuerystringIdentifier() + { + return 'authorization_code'; + } + + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$request->request('code')) { + $response->setError(400, 'invalid_request', 'Missing parameter: "code" is required'); + + return false; + } + + $code = $request->request('code'); + if (!$authCode = $this->storage->getAuthorizationCode($code)) { + $response->setError(400, 'invalid_grant', 'Authorization code doesn\'t exist or is invalid for the client'); + + return false; + } + + /* + * 4.1.3 - ensure that the "redirect_uri" parameter is present if the "redirect_uri" parameter was included in the initial authorization request + * @uri - http://tools.ietf.org/html/rfc6749#section-4.1.3 + */ + if (isset($authCode['redirect_uri']) && $authCode['redirect_uri']) { + if (!$request->request('redirect_uri') || urldecode($request->request('redirect_uri')) != $authCode['redirect_uri']) { + $response->setError(400, 'redirect_uri_mismatch', "The redirect URI is missing or do not match", "#section-4.1.3"); + + return false; + } + } + + if (!isset($authCode['expires'])) { + throw new \Exception('Storage must return authcode with a value for "expires"'); + } + + if ($authCode["expires"] < time()) { + $response->setError(400, 'invalid_grant', "The authorization code has expired"); + + return false; + } + + if (!isset($authCode['code'])) { + $authCode['code'] = $code; // used to expire the code after the access token is granted + } + + $this->authCode = $authCode; + + return true; + } + + public function getClientId() + { + return $this->authCode['client_id']; + } + + public function getScope() + { + return isset($this->authCode['scope']) ? $this->authCode['scope'] : null; + } + + public function getUserId() + { + return isset($this->authCode['user_id']) ? $this->authCode['user_id'] : null; + } + + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + $token = $accessToken->createAccessToken($client_id, $user_id, $scope); + $this->storage->expireAuthorizationCode($this->authCode['code']); + + return $token; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/ClientCredentials.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/ClientCredentials.php new file mode 100644 index 0000000..f953e4e --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/ClientCredentials.php @@ -0,0 +1,67 @@ + + * + * @see OAuth2\ClientAssertionType_HttpBasic + */ +class ClientCredentials extends HttpBasic implements GrantTypeInterface +{ + private $clientData; + + public function __construct(ClientCredentialsInterface $storage, array $config = array()) + { + /** + * The client credentials grant type MUST only be used by confidential clients + * + * @see http://tools.ietf.org/html/rfc6749#section-4.4 + */ + $config['allow_public_clients'] = false; + + parent::__construct($storage, $config); + } + + public function getQuerystringIdentifier() + { + return 'client_credentials'; + } + + public function getScope() + { + $this->loadClientData(); + + return isset($this->clientData['scope']) ? $this->clientData['scope'] : null; + } + + public function getUserId() + { + $this->loadClientData(); + + return isset($this->clientData['user_id']) ? $this->clientData['user_id'] : null; + } + + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + /** + * Client Credentials Grant does NOT include a refresh token + * + * @see http://tools.ietf.org/html/rfc6749#section-4.4.3 + */ + $includeRefreshToken = false; + + return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken); + } + + private function loadClientData() + { + if (!$this->clientData) { + $this->clientData = $this->storage->getClientDetails($this->getClientId()); + } + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/GrantTypeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/GrantTypeInterface.php new file mode 100644 index 0000000..98489e9 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/GrantTypeInterface.php @@ -0,0 +1,20 @@ + + */ +class JwtBearer implements GrantTypeInterface, ClientAssertionTypeInterface +{ + private $jwt; + + protected $storage; + protected $audience; + protected $jwtUtil; + protected $allowedAlgorithms; + + /** + * Creates an instance of the JWT bearer grant type. + * + * @param OAuth2\Storage\JWTBearerInterface|JwtBearerInterface $storage A valid storage interface that implements storage hooks for the JWT bearer grant type. + * @param string $audience The audience to validate the token against. This is usually the full URI of the OAuth token requests endpoint. + * @param EncryptionInterface|OAuth2\Encryption\JWT $jwtUtil OPTONAL The class used to decode, encode and verify JWTs. + * @param array $config + */ + public function __construct(JwtBearerInterface $storage, $audience, EncryptionInterface $jwtUtil = null, array $config = array()) + { + $this->storage = $storage; + $this->audience = $audience; + + if (is_null($jwtUtil)) { + $jwtUtil = new Jwt(); + } + + $this->config = array_merge(array( + 'allowed_algorithms' => array('RS256', 'RS384', 'RS512') + ), $config); + + $this->jwtUtil = $jwtUtil; + + $this->allowedAlgorithms = $this->config['allowed_algorithms']; + } + + /** + * Returns the grant_type get parameter to identify the grant type request as JWT bearer authorization grant. + * + * @return + * The string identifier for grant_type. + * + * @see OAuth2\GrantType\GrantTypeInterface::getQuerystringIdentifier() + */ + public function getQuerystringIdentifier() + { + return 'urn:ietf:params:oauth:grant-type:jwt-bearer'; + } + + /** + * Validates the data from the decoded JWT. + * + * @return + * TRUE if the JWT request is valid and can be decoded. Otherwise, FALSE is returned. + * + * @see OAuth2\GrantType\GrantTypeInterface::getTokenData() + */ + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$request->request("assertion")) { + $response->setError(400, 'invalid_request', 'Missing parameters: "assertion" required'); + + return null; + } + + // Store the undecoded JWT for later use + $undecodedJWT = $request->request('assertion'); + + // Decode the JWT + $jwt = $this->jwtUtil->decode($request->request('assertion'), null, false); + + if (!$jwt) { + $response->setError(400, 'invalid_request', "JWT is malformed"); + + return null; + } + + // ensure these properties contain a value + // @todo: throw malformed error for missing properties + $jwt = array_merge(array( + 'scope' => null, + 'iss' => null, + 'sub' => null, + 'aud' => null, + 'exp' => null, + 'nbf' => null, + 'iat' => null, + 'jti' => null, + 'typ' => null, + ), $jwt); + + if (!isset($jwt['iss'])) { + $response->setError(400, 'invalid_grant', "Invalid issuer (iss) provided"); + + return null; + } + + if (!isset($jwt['sub'])) { + $response->setError(400, 'invalid_grant', "Invalid subject (sub) provided"); + + return null; + } + + if (!isset($jwt['exp'])) { + $response->setError(400, 'invalid_grant', "Expiration (exp) time must be present"); + + return null; + } + + // Check expiration + if (ctype_digit($jwt['exp'])) { + if ($jwt['exp'] <= time()) { + $response->setError(400, 'invalid_grant', "JWT has expired"); + + return null; + } + } else { + $response->setError(400, 'invalid_grant', "Expiration (exp) time must be a unix time stamp"); + + return null; + } + + // Check the not before time + if ($notBefore = $jwt['nbf']) { + if (ctype_digit($notBefore)) { + if ($notBefore > time()) { + $response->setError(400, 'invalid_grant', "JWT cannot be used before the Not Before (nbf) time"); + + return null; + } + } else { + $response->setError(400, 'invalid_grant', "Not Before (nbf) time must be a unix time stamp"); + + return null; + } + } + + // Check the audience if required to match + if (!isset($jwt['aud']) || ($jwt['aud'] != $this->audience)) { + $response->setError(400, 'invalid_grant', "Invalid audience (aud)"); + + return null; + } + + // Check the jti (nonce) + // @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-13#section-4.1.7 + if (isset($jwt['jti'])) { + $jti = $this->storage->getJti($jwt['iss'], $jwt['sub'], $jwt['aud'], $jwt['exp'], $jwt['jti']); + + //Reject if jti is used and jwt is still valid (exp parameter has not expired). + if ($jti && $jti['expires'] > time()) { + $response->setError(400, 'invalid_grant', "JSON Token Identifier (jti) has already been used"); + + return null; + } else { + $this->storage->setJti($jwt['iss'], $jwt['sub'], $jwt['aud'], $jwt['exp'], $jwt['jti']); + } + } + + // Get the iss's public key + // @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06#section-4.1.1 + if (!$key = $this->storage->getClientKey($jwt['iss'], $jwt['sub'])) { + $response->setError(400, 'invalid_grant', "Invalid issuer (iss) or subject (sub) provided"); + + return null; + } + + // Verify the JWT + if (!$this->jwtUtil->decode($undecodedJWT, $key, $this->allowedAlgorithms)) { + $response->setError(400, 'invalid_grant', "JWT failed signature verification"); + + return null; + } + + $this->jwt = $jwt; + + return true; + } + + public function getClientId() + { + return $this->jwt['iss']; + } + + public function getUserId() + { + return $this->jwt['sub']; + } + + public function getScope() + { + return null; + } + + /** + * Creates an access token that is NOT associated with a refresh token. + * If a subject (sub) the name of the user/account we are accessing data on behalf of. + * + * @see OAuth2\GrantType\GrantTypeInterface::createAccessToken() + */ + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + $includeRefreshToken = false; + + return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/RefreshToken.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/RefreshToken.php new file mode 100644 index 0000000..e553852 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/RefreshToken.php @@ -0,0 +1,111 @@ + + */ +class RefreshToken implements GrantTypeInterface +{ + private $refreshToken; + + protected $storage; + protected $config; + + /** + * @param OAuth2\Storage\RefreshTokenInterface $storage REQUIRED Storage class for retrieving refresh token information + * @param array $config OPTIONAL Configuration options for the server + * + * $config = array( + * 'always_issue_new_refresh_token' => true, // whether to issue a new refresh token upon successful token request + * 'unset_refresh_token_after_use' => true // whether to unset the refresh token after after using + * ); + * + */ + public function __construct(RefreshTokenInterface $storage, $config = array()) + { + $this->config = array_merge(array( + 'always_issue_new_refresh_token' => false, + 'unset_refresh_token_after_use' => true + ), $config); + + // to preserve B.C. with v1.6 + // @see https://github.com/bshaffer/oauth2-server-php/pull/580 + // @todo - remove in v2.0 + if (isset($config['always_issue_new_refresh_token']) && !isset($config['unset_refresh_token_after_use'])) { + $this->config['unset_refresh_token_after_use'] = $config['always_issue_new_refresh_token']; + } + + $this->storage = $storage; + } + + public function getQuerystringIdentifier() + { + return 'refresh_token'; + } + + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$request->request("refresh_token")) { + $response->setError(400, 'invalid_request', 'Missing parameter: "refresh_token" is required'); + + return null; + } + + if (!$refreshToken = $this->storage->getRefreshToken($request->request("refresh_token"))) { + $response->setError(400, 'invalid_grant', 'Invalid refresh token'); + + return null; + } + + if ($refreshToken['expires'] > 0 && $refreshToken["expires"] < time()) { + $response->setError(400, 'invalid_grant', 'Refresh token has expired'); + + return null; + } + + // store the refresh token locally so we can delete it when a new refresh token is generated + $this->refreshToken = $refreshToken; + + return true; + } + + public function getClientId() + { + return $this->refreshToken['client_id']; + } + + public function getUserId() + { + return isset($this->refreshToken['user_id']) ? $this->refreshToken['user_id'] : null; + } + + public function getScope() + { + return isset($this->refreshToken['scope']) ? $this->refreshToken['scope'] : null; + } + + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + /* + * It is optional to force a new refresh token when a refresh token is used. + * However, if a new refresh token is issued, the old one MUST be expired + * @see http://tools.ietf.org/html/rfc6749#section-6 + */ + $issueNewRefreshToken = $this->config['always_issue_new_refresh_token']; + $unsetRefreshToken = $this->config['unset_refresh_token_after_use']; + $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $issueNewRefreshToken); + + if ($unsetRefreshToken) { + $this->storage->unsetRefreshToken($this->refreshToken['refresh_token']); + } + + return $token; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/UserCredentials.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/UserCredentials.php new file mode 100644 index 0000000..f165538 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/UserCredentials.php @@ -0,0 +1,83 @@ + + */ +class UserCredentials implements GrantTypeInterface +{ + private $userInfo; + + protected $storage; + + /** + * @param OAuth2\Storage\UserCredentialsInterface $storage REQUIRED Storage class for retrieving user credentials information + */ + public function __construct(UserCredentialsInterface $storage) + { + $this->storage = $storage; + } + + public function getQuerystringIdentifier() + { + return 'password'; + } + + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$request->request("password") || !$request->request("username")) { + $response->setError(400, 'invalid_request', 'Missing parameters: "username" and "password" required'); + + return null; + } + + if (!$this->storage->checkUserCredentials($request->request("username"), $request->request("password"))) { + $response->setError(401, 'invalid_grant', 'Invalid username and password combination'); + + return null; + } + + $userInfo = $this->storage->getUserDetails($request->request("username")); + + if (empty($userInfo)) { + $response->setError(400, 'invalid_grant', 'Unable to retrieve user information'); + + return null; + } + + if (!isset($userInfo['user_id'])) { + throw new \LogicException("you must set the user_id on the array returned by getUserDetails"); + } + + $this->userInfo = $userInfo; + + return true; + } + + public function getClientId() + { + return null; + } + + public function getUserId() + { + return $this->userInfo['user_id']; + } + + public function getScope() + { + return isset($this->userInfo['scope']) ? $this->userInfo['scope'] : null; + } + + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + return $accessToken->createAccessToken($client_id, $user_id, $scope); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeController.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeController.php new file mode 100644 index 0000000..c9b5c6a --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeController.php @@ -0,0 +1,106 @@ +query('prompt', 'consent'); + if ($prompt == 'none') { + if (is_null($user_id)) { + $error = 'login_required'; + $error_message = 'The user must log in'; + } else { + $error = 'interaction_required'; + $error_message = 'The user must grant access to your application'; + } + } else { + $error = 'consent_required'; + $error_message = 'The user denied access to your application'; + } + + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->getState(), $error, $error_message); + } + + protected function buildAuthorizeParameters($request, $response, $user_id) + { + if (!$params = parent::buildAuthorizeParameters($request, $response, $user_id)) { + return; + } + + // Generate an id token if needed. + if ($this->needsIdToken($this->getScope()) && $this->getResponseType() == self::RESPONSE_TYPE_AUTHORIZATION_CODE) { + $params['id_token'] = $this->responseTypes['id_token']->createIdToken($this->getClientId(), $user_id, $this->nonce); + } + + // add the nonce to return with the redirect URI + $params['nonce'] = $this->nonce; + + return $params; + } + + public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response) + { + if (!parent::validateAuthorizeRequest($request, $response)) { + return false; + } + + $nonce = $request->query('nonce'); + + // Validate required nonce for "id_token" and "id_token token" + if (!$nonce && in_array($this->getResponseType(), array(self::RESPONSE_TYPE_ID_TOKEN, self::RESPONSE_TYPE_ID_TOKEN_TOKEN))) { + $response->setError(400, 'invalid_nonce', 'This application requires you specify a nonce parameter'); + + return false; + } + + $this->nonce = $nonce; + + return true; + } + + protected function getValidResponseTypes() + { + return array( + self::RESPONSE_TYPE_ACCESS_TOKEN, + self::RESPONSE_TYPE_AUTHORIZATION_CODE, + self::RESPONSE_TYPE_ID_TOKEN, + self::RESPONSE_TYPE_ID_TOKEN_TOKEN, + self::RESPONSE_TYPE_CODE_ID_TOKEN, + ); + } + + /** + * Returns whether the current request needs to generate an id token. + * + * ID Tokens are a part of the OpenID Connect specification, so this + * method checks whether OpenID Connect is enabled in the server settings + * and whether the openid scope was requested. + * + * @param $request_scope + * A space-separated string of scopes. + * + * @return + * TRUE if an id token is needed, FALSE otherwise. + */ + public function needsIdToken($request_scope) + { + // see if the "openid" scope exists in the requested scope + return $this->scopeUtil->checkScope('openid', $request_scope); + } + + public function getNonce() + { + return $this->nonce; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php new file mode 100644 index 0000000..1e231d8 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php @@ -0,0 +1,10 @@ +tokenType = $tokenType; + $this->tokenStorage = $tokenStorage; + $this->userClaimsStorage = $userClaimsStorage; + + $this->config = array_merge(array( + 'www_realm' => 'Service', + ), $config); + + if (is_null($scopeUtil)) { + $scopeUtil = new Scope(); + } + $this->scopeUtil = $scopeUtil; + } + + public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$this->verifyResourceRequest($request, $response, 'openid')) { + return; + } + + $token = $this->getToken(); + $claims = $this->userClaimsStorage->getUserClaims($token['user_id'], $token['scope']); + // The sub Claim MUST always be returned in the UserInfo Response. + // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse + $claims += array( + 'sub' => $token['user_id'], + ); + $response->addParameters($claims); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php new file mode 100644 index 0000000..a89049d --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php @@ -0,0 +1,23 @@ + $response = new OAuth2\Response(); + * > $userInfoController->handleUserInfoRequest( + * > OAuth2\Request::createFromGlobals(), + * > $response; + * > $response->send(); + * + */ +interface UserInfoControllerInterface +{ + public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/GrantType/AuthorizationCode.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/GrantType/AuthorizationCode.php new file mode 100644 index 0000000..8ed1edc --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/GrantType/AuthorizationCode.php @@ -0,0 +1,33 @@ + + */ +class AuthorizationCode extends BaseAuthorizationCode +{ + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + $includeRefreshToken = true; + if (isset($this->authCode['id_token'])) { + // OpenID Connect requests include the refresh token only if the + // offline_access scope has been requested and granted. + $scopes = explode(' ', trim($scope)); + $includeRefreshToken = in_array('offline_access', $scopes); + } + + $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken); + if (isset($this->authCode['id_token'])) { + $token['id_token'] = $this->authCode['id_token']; + } + + $this->storage->expireAuthorizationCode($this->authCode['code']); + + return $token; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php new file mode 100644 index 0000000..8971954 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php @@ -0,0 +1,60 @@ + + */ +class AuthorizationCode extends BaseAuthorizationCode implements AuthorizationCodeInterface +{ + public function __construct(AuthorizationCodeStorageInterface $storage, array $config = array()) + { + parent::__construct($storage, $config); + } + + public function getAuthorizeResponse($params, $user_id = null) + { + // build the URL to redirect to + $result = array('query' => array()); + + $params += array('scope' => null, 'state' => null, 'id_token' => null); + + $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['id_token']); + + if (isset($params['state'])) { + $result['query']['state'] = $params['state']; + } + + return array($params['redirect_uri'], $result); + } + + /** + * Handle the creation of the authorization code. + * + * @param $client_id + * Client identifier related to the authorization code + * @param $user_id + * User ID associated with the authorization code + * @param $redirect_uri + * An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * @param $id_token + * (optional) The OpenID Connect id_token. + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @ingroup oauth2_section_4 + */ + public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null) + { + $code = $this->generateAuthorizationCode(); + $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, $id_token); + + return $code; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php new file mode 100644 index 0000000..ea47792 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php @@ -0,0 +1,27 @@ + + */ +interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface +{ + /** + * Handle the creation of the authorization code. + * + * @param $client_id Client identifier related to the authorization code + * @param $user_id User ID associated with the authorization code + * @param $redirect_uri An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param $scope OPTIONAL Scopes to be stored in space-separated string. + * @param $id_token OPTIONAL The OpenID Connect id_token. + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @ingroup oauth2_section_4 + */ + public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdToken.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdToken.php new file mode 100644 index 0000000..9bc3322 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdToken.php @@ -0,0 +1,24 @@ +authCode = $authCode; + $this->idToken = $idToken; + } + + public function getAuthorizeResponse($params, $user_id = null) + { + $result = $this->authCode->getAuthorizeResponse($params, $user_id); + $id_token = $this->idToken->createIdToken($params['client_id'], $user_id, $params['nonce']); + $result[1]['query']['id_token'] = $id_token; + + return $result; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php new file mode 100644 index 0000000..629adcc --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php @@ -0,0 +1,9 @@ +userClaimsStorage = $userClaimsStorage; + $this->publicKeyStorage = $publicKeyStorage; + if (is_null($encryptionUtil)) { + $encryptionUtil = new Jwt(); + } + $this->encryptionUtil = $encryptionUtil; + + if (!isset($config['issuer'])) { + throw new \LogicException('config parameter "issuer" must be set'); + } + $this->config = array_merge(array( + 'id_lifetime' => 3600, + ), $config); + } + + public function getAuthorizeResponse($params, $userInfo = null) + { + // build the URL to redirect to + $result = array('query' => array()); + $params += array('scope' => null, 'state' => null, 'nonce' => null); + + // create the id token. + list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo); + $userClaims = $this->userClaimsStorage->getUserClaims($user_id, $params['scope']); + + $id_token = $this->createIdToken($params['client_id'], $userInfo, $params['nonce'], $userClaims, null); + $result["fragment"] = array('id_token' => $id_token); + if (isset($params['state'])) { + $result["fragment"]["state"] = $params['state']; + } + + return array($params['redirect_uri'], $result); + } + + public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims = null, $access_token = null) + { + // pull auth_time from user info if supplied + list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo); + + $token = array( + 'iss' => $this->config['issuer'], + 'sub' => $user_id, + 'aud' => $client_id, + 'iat' => time(), + 'exp' => time() + $this->config['id_lifetime'], + 'auth_time' => $auth_time, + ); + + if ($nonce) { + $token['nonce'] = $nonce; + } + + if ($userClaims) { + $token += $userClaims; + } + + if ($access_token) { + $token['at_hash'] = $this->createAtHash($access_token, $client_id); + } + + return $this->encodeToken($token, $client_id); + } + + protected function createAtHash($access_token, $client_id = null) + { + // maps HS256 and RS256 to sha256, etc. + $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); + $hash_algorithm = 'sha' . substr($algorithm, 2); + $hash = hash($hash_algorithm, $access_token); + $at_hash = substr($hash, 0, strlen($hash) / 2); + + return $this->encryptionUtil->urlSafeB64Encode($at_hash); + } + + protected function encodeToken(array $token, $client_id = null) + { + $private_key = $this->publicKeyStorage->getPrivateKey($client_id); + $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); + + return $this->encryptionUtil->encode($token, $private_key, $algorithm); + } + + private function getUserIdAndAuthTime($userInfo) + { + $auth_time = null; + + // support an array for user_id / auth_time + if (is_array($userInfo)) { + if (!isset($userInfo['user_id'])) { + throw new \LogicException('if $user_id argument is an array, user_id index must be set'); + } + + $auth_time = isset($userInfo['auth_time']) ? $userInfo['auth_time'] : null; + $user_id = $userInfo['user_id']; + } else { + $user_id = $userInfo; + } + + if (is_null($auth_time)) { + $auth_time = time(); + } + + // userInfo is a scalar, and so this is the $user_id. Auth Time is null + return array($user_id, $auth_time); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php new file mode 100644 index 0000000..0bd2f83 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php @@ -0,0 +1,29 @@ +accessToken = $accessToken; + $this->idToken = $idToken; + } + + public function getAuthorizeResponse($params, $user_id = null) + { + $result = $this->accessToken->getAuthorizeResponse($params, $user_id); + $access_token = $result[1]['fragment']['access_token']; + $id_token = $this->idToken->createIdToken($params['client_id'], $user_id, $params['nonce'], null, $access_token); + $result[1]['fragment']['id_token'] = $id_token; + + return $result; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php new file mode 100644 index 0000000..ac13e20 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php @@ -0,0 +1,9 @@ + + */ +interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface +{ + /** + * Take the provided authorization code values and store them somewhere. + * + * This function should be the storage counterpart to getAuthCode(). + * + * If storage fails for some reason, we're not currently checking for + * any sort of success/failure, so you should bail out of the script + * and provide a descriptive fail message. + * + * Required for OAuth2::GRANT_TYPE_AUTH_CODE. + * + * @param $code authorization code to be stored. + * @param $client_id client identifier to be stored. + * @param $user_id user identifier to be stored. + * @param string $redirect_uri redirect URI(s) to be stored in a space-separated string. + * @param int $expires expiration to be stored as a Unix timestamp. + * @param string $scope OPTIONAL scopes to be stored in space-separated string. + * @param string $id_token OPTIONAL the OpenID Connect id_token. + * + * @ingroup oauth2_section_4 + */ + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/UserClaimsInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/UserClaimsInterface.php new file mode 100644 index 0000000..f230bef --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/UserClaimsInterface.php @@ -0,0 +1,38 @@ + value format. + * + * @see http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + */ + public function getUserClaims($user_id, $scope); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Request.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Request.php new file mode 100644 index 0000000..c92cee8 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Request.php @@ -0,0 +1,213 @@ +initialize($query, $request, $attributes, $cookies, $files, $server, $content, $headers); + } + + /** + * Sets the parameters for this request. + * + * This method also re-initializes all properties. + * + * @param array $query The GET parameters + * @param array $request The POST parameters + * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies The COOKIE parameters + * @param array $files The FILES parameters + * @param array $server The SERVER parameters + * @param string $content The raw body data + * + * @api + */ + public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null, array $headers = null) + { + $this->request = $request; + $this->query = $query; + $this->attributes = $attributes; + $this->cookies = $cookies; + $this->files = $files; + $this->server = $server; + $this->content = $content; + $this->headers = is_null($headers) ? $this->getHeadersFromServer($this->server) : $headers; + } + + public function query($name, $default = null) + { + return isset($this->query[$name]) ? $this->query[$name] : $default; + } + + public function request($name, $default = null) + { + return isset($this->request[$name]) ? $this->request[$name] : $default; + } + + public function server($name, $default = null) + { + return isset($this->server[$name]) ? $this->server[$name] : $default; + } + + public function headers($name, $default = null) + { + $headers = array_change_key_case($this->headers); + $name = strtolower($name); + + return isset($headers[$name]) ? $headers[$name] : $default; + } + + public function getAllQueryParameters() + { + return $this->query; + } + + /** + * Returns the request body content. + * + * @param Boolean $asResource If true, a resource will be returned + * + * @return string|resource The request body content or a resource to read the body stream. + */ + public function getContent($asResource = false) + { + if (false === $this->content || (true === $asResource && null !== $this->content)) { + throw new \LogicException('getContent() can only be called once when using the resource return type.'); + } + + if (true === $asResource) { + $this->content = false; + + return fopen('php://input', 'rb'); + } + + if (null === $this->content) { + $this->content = file_get_contents('php://input'); + } + + return $this->content; + } + + private function getHeadersFromServer($server) + { + $headers = array(); + foreach ($server as $key => $value) { + if (0 === strpos($key, 'HTTP_')) { + $headers[substr($key, 5)] = $value; + } + // CONTENT_* are not prefixed with HTTP_ + elseif (in_array($key, array('CONTENT_LENGTH', 'CONTENT_MD5', 'CONTENT_TYPE'))) { + $headers[$key] = $value; + } + } + + if (isset($server['PHP_AUTH_USER'])) { + $headers['PHP_AUTH_USER'] = $server['PHP_AUTH_USER']; + $headers['PHP_AUTH_PW'] = isset($server['PHP_AUTH_PW']) ? $server['PHP_AUTH_PW'] : ''; + } else { + /* + * php-cgi under Apache does not pass HTTP Basic user/pass to PHP by default + * For this workaround to work, add this line to your .htaccess file: + * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + * + * A sample .htaccess file: + * RewriteEngine On + * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + * RewriteCond %{REQUEST_FILENAME} !-f + * RewriteRule ^(.*)$ app.php [QSA,L] + */ + + $authorizationHeader = null; + if (isset($server['HTTP_AUTHORIZATION'])) { + $authorizationHeader = $server['HTTP_AUTHORIZATION']; + } elseif (isset($server['REDIRECT_HTTP_AUTHORIZATION'])) { + $authorizationHeader = $server['REDIRECT_HTTP_AUTHORIZATION']; + } elseif (function_exists('apache_request_headers')) { + $requestHeaders = (array) apache_request_headers(); + + // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization) + $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders)); + + if (isset($requestHeaders['Authorization'])) { + $authorizationHeader = trim($requestHeaders['Authorization']); + } + } + + if (null !== $authorizationHeader) { + $headers['AUTHORIZATION'] = $authorizationHeader; + // Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic + if (0 === stripos($authorizationHeader, 'basic')) { + $exploded = explode(':', base64_decode(substr($authorizationHeader, 6))); + if (count($exploded) == 2) { + list($headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']) = $exploded; + } + } + } + } + + // PHP_AUTH_USER/PHP_AUTH_PW + if (isset($headers['PHP_AUTH_USER'])) { + $headers['AUTHORIZATION'] = 'Basic '.base64_encode($headers['PHP_AUTH_USER'].':'.$headers['PHP_AUTH_PW']); + } + + return $headers; + } + + /** + * Creates a new request with values from PHP's super globals. + * + * @return Request A new request + * + * @api + */ + public static function createFromGlobals() + { + $class = get_called_class(); + $request = new $class($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER); + + $contentType = $request->server('CONTENT_TYPE', ''); + $requestMethod = $request->server('REQUEST_METHOD', 'GET'); + if (0 === strpos($contentType, 'application/x-www-form-urlencoded') + && in_array(strtoupper($requestMethod), array('PUT', 'DELETE')) + ) { + parse_str($request->getContent(), $data); + $request->request = $data; + } elseif (0 === strpos($contentType, 'application/json') + && in_array(strtoupper($requestMethod), array('POST', 'PUT', 'DELETE')) + ) { + $data = json_decode($request->getContent(), true); + $request->request = $data; + } + + return $request; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/RequestInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/RequestInterface.php new file mode 100644 index 0000000..8a70d5f --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/RequestInterface.php @@ -0,0 +1,16 @@ + 'Continue', + 101 => 'Switching Protocols', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + ); + + public function __construct($parameters = array(), $statusCode = 200, $headers = array()) + { + $this->setParameters($parameters); + $this->setStatusCode($statusCode); + $this->setHttpHeaders($headers); + $this->version = '1.1'; + } + + /** + * Converts the response object to string containing all headers and the response content. + * + * @return string The response with headers and content + */ + public function __toString() + { + $headers = array(); + foreach ($this->httpHeaders as $name => $value) { + $headers[$name] = (array) $value; + } + + return + sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n". + $this->getHttpHeadersAsString($headers)."\r\n". + $this->getResponseBody(); + } + + /** + * Returns the build header line. + * + * @param string $name The header name + * @param string $value The header value + * + * @return string The built header line + */ + protected function buildHeader($name, $value) + { + return sprintf("%s: %s\n", $name, $value); + } + + public function getStatusCode() + { + return $this->statusCode; + } + + public function setStatusCode($statusCode, $text = null) + { + $this->statusCode = (int) $statusCode; + if ($this->isInvalid()) { + throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $statusCode)); + } + + $this->statusText = false === $text ? '' : (null === $text ? self::$statusTexts[$this->statusCode] : $text); + } + + public function getStatusText() + { + return $this->statusText; + } + + public function getParameters() + { + return $this->parameters; + } + + public function setParameters(array $parameters) + { + $this->parameters = $parameters; + } + + public function addParameters(array $parameters) + { + $this->parameters = array_merge($this->parameters, $parameters); + } + + public function getParameter($name, $default = null) + { + return isset($this->parameters[$name]) ? $this->parameters[$name] : $default; + } + + public function setParameter($name, $value) + { + $this->parameters[$name] = $value; + } + + public function setHttpHeaders(array $httpHeaders) + { + $this->httpHeaders = $httpHeaders; + } + + public function setHttpHeader($name, $value) + { + $this->httpHeaders[$name] = $value; + } + + public function addHttpHeaders(array $httpHeaders) + { + $this->httpHeaders = array_merge($this->httpHeaders, $httpHeaders); + } + + public function getHttpHeaders() + { + return $this->httpHeaders; + } + + public function getHttpHeader($name, $default = null) + { + return isset($this->httpHeaders[$name]) ? $this->httpHeaders[$name] : $default; + } + + public function getResponseBody($format = 'json') + { + switch ($format) { + case 'json': + return json_encode($this->parameters); + case 'xml': + // this only works for single-level arrays + $xml = new \SimpleXMLElement(''); + foreach ($this->parameters as $key => $param) { + $xml->addChild($key, $param); + } + + return $xml->asXML(); + } + + throw new \InvalidArgumentException(sprintf('The format %s is not supported', $format)); + + } + + public function send($format = 'json') + { + // headers have already been sent by the developer + if (headers_sent()) { + return; + } + + switch ($format) { + case 'json': + $this->setHttpHeader('Content-Type', 'application/json'); + break; + case 'xml': + $this->setHttpHeader('Content-Type', 'text/xml'); + break; + } + // status + header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)); + + foreach ($this->getHttpHeaders() as $name => $header) { + header(sprintf('%s: %s', $name, $header)); + } + echo $this->getResponseBody($format); + } + + public function setError($statusCode, $error, $errorDescription = null, $errorUri = null) + { + $parameters = array( + 'error' => $error, + 'error_description' => $errorDescription, + ); + + if (!is_null($errorUri)) { + if (strlen($errorUri) > 0 && $errorUri[0] == '#') { + // we are referencing an oauth bookmark (for brevity) + $errorUri = 'http://tools.ietf.org/html/rfc6749' . $errorUri; + } + $parameters['error_uri'] = $errorUri; + } + + $httpHeaders = array( + 'Cache-Control' => 'no-store' + ); + + $this->setStatusCode($statusCode); + $this->addParameters($parameters); + $this->addHttpHeaders($httpHeaders); + + if (!$this->isClientError() && !$this->isServerError()) { + throw new \InvalidArgumentException(sprintf('The HTTP status code is not an error ("%s" given).', $statusCode)); + } + } + + public function setRedirect($statusCode, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null) + { + if (empty($url)) { + throw new \InvalidArgumentException('Cannot redirect to an empty URL.'); + } + + $parameters = array(); + + if (!is_null($state)) { + $parameters['state'] = $state; + } + + if (!is_null($error)) { + $this->setError(400, $error, $errorDescription, $errorUri); + } + $this->setStatusCode($statusCode); + $this->addParameters($parameters); + + if (count($this->parameters) > 0) { + // add parameters to URL redirection + $parts = parse_url($url); + $sep = isset($parts['query']) && count($parts['query']) > 0 ? '&' : '?'; + $url .= $sep . http_build_query($this->parameters); + } + + $this->addHttpHeaders(array('Location' => $url)); + + if (!$this->isRedirection()) { + throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $statusCode)); + } + } + + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + /** + * @return Boolean + * + * @api + */ + public function isInvalid() + { + return $this->statusCode < 100 || $this->statusCode >= 600; + } + + /** + * @return Boolean + * + * @api + */ + public function isInformational() + { + return $this->statusCode >= 100 && $this->statusCode < 200; + } + + /** + * @return Boolean + * + * @api + */ + public function isSuccessful() + { + return $this->statusCode >= 200 && $this->statusCode < 300; + } + + /** + * @return Boolean + * + * @api + */ + public function isRedirection() + { + return $this->statusCode >= 300 && $this->statusCode < 400; + } + + /** + * @return Boolean + * + * @api + */ + public function isClientError() + { + return $this->statusCode >= 400 && $this->statusCode < 500; + } + + /** + * @return Boolean + * + * @api + */ + public function isServerError() + { + return $this->statusCode >= 500 && $this->statusCode < 600; + } + + /* + * Functions from Symfony2 HttpFoundation - output pretty header + */ + private function getHttpHeadersAsString($headers) + { + if (count($headers) == 0) { + return ''; + } + + $max = max(array_map('strlen', array_keys($headers))) + 1; + $content = ''; + ksort($headers); + foreach ($headers as $name => $values) { + foreach ($values as $value) { + $content .= sprintf("%-{$max}s %s\r\n", $this->beautifyHeaderName($name).':', $value); + } + } + + return $content; + } + + private function beautifyHeaderName($name) + { + return preg_replace_callback('/\-(.)/', array($this, 'beautifyCallback'), ucfirst($name)); + } + + private function beautifyCallback($match) + { + return '-'.strtoupper($match[1]); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseInterface.php new file mode 100644 index 0000000..c99b5f7 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseInterface.php @@ -0,0 +1,24 @@ + + */ +class AccessToken implements AccessTokenInterface +{ + protected $tokenStorage; + protected $refreshStorage; + protected $config; + + /** + * @param OAuth2\Storage\AccessTokenInterface $tokenStorage REQUIRED Storage class for saving access token information + * @param OAuth2\Storage\RefreshTokenInterface $refreshStorage OPTIONAL Storage class for saving refresh token information + * @param array $config OPTIONAL Configuration options for the server + * + * $config = array( + * 'token_type' => 'bearer', // token type identifier + * 'access_lifetime' => 3600, // time before access token expires + * 'refresh_token_lifetime' => 1209600, // time before refresh token expires + * ); + * + */ + public function __construct(AccessTokenStorageInterface $tokenStorage, RefreshTokenInterface $refreshStorage = null, array $config = array()) + { + $this->tokenStorage = $tokenStorage; + $this->refreshStorage = $refreshStorage; + + $this->config = array_merge(array( + 'token_type' => 'bearer', + 'access_lifetime' => 3600, + 'refresh_token_lifetime' => 1209600, + ), $config); + } + + public function getAuthorizeResponse($params, $user_id = null) + { + // build the URL to redirect to + $result = array('query' => array()); + + $params += array('scope' => null, 'state' => null); + + /* + * a refresh token MUST NOT be included in the fragment + * + * @see http://tools.ietf.org/html/rfc6749#section-4.2.2 + */ + $includeRefreshToken = false; + $result["fragment"] = $this->createAccessToken($params['client_id'], $user_id, $params['scope'], $includeRefreshToken); + + if (isset($params['state'])) { + $result["fragment"]["state"] = $params['state']; + } + + return array($params['redirect_uri'], $result); + } + + /** + * Handle the creation of access token, also issue refresh token if supported / desirable. + * + * @param $client_id client identifier related to the access token. + * @param $user_id user ID associated with the access token + * @param $scope OPTIONAL scopes to be stored in space-separated string. + * @param bool $includeRefreshToken if true, a new refresh_token will be added to the response + * + * @see http://tools.ietf.org/html/rfc6749#section-5 + * @ingroup oauth2_section_5 + */ + public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true) + { + $token = array( + "access_token" => $this->generateAccessToken(), + "expires_in" => $this->config['access_lifetime'], + "token_type" => $this->config['token_type'], + "scope" => $scope + ); + + $this->tokenStorage->setAccessToken($token["access_token"], $client_id, $user_id, $this->config['access_lifetime'] ? time() + $this->config['access_lifetime'] : null, $scope); + + /* + * Issue a refresh token also, if we support them + * + * Refresh Tokens are considered supported if an instance of OAuth2\Storage\RefreshTokenInterface + * is supplied in the constructor + */ + if ($includeRefreshToken && $this->refreshStorage) { + $token["refresh_token"] = $this->generateRefreshToken(); + $expires = 0; + if ($this->config['refresh_token_lifetime'] > 0) { + $expires = time() + $this->config['refresh_token_lifetime']; + } + $this->refreshStorage->setRefreshToken($token['refresh_token'], $client_id, $user_id, $expires, $scope); + } + + return $token; + } + + /** + * Generates an unique access token. + * + * Implementing classes may want to override this function to implement + * other access token generation schemes. + * + * @return + * An unique access token. + * + * @ingroup oauth2_section_4 + */ + protected function generateAccessToken() + { + if (function_exists('mcrypt_create_iv')) { + $randomData = mcrypt_create_iv(20, MCRYPT_DEV_URANDOM); + if ($randomData !== false && strlen($randomData) === 20) { + return bin2hex($randomData); + } + } + if (function_exists('openssl_random_pseudo_bytes')) { + $randomData = openssl_random_pseudo_bytes(20); + if ($randomData !== false && strlen($randomData) === 20) { + return bin2hex($randomData); + } + } + if (@file_exists('/dev/urandom')) { // Get 100 bytes of random data + $randomData = file_get_contents('/dev/urandom', false, null, 0, 20); + if ($randomData !== false && strlen($randomData) === 20) { + return bin2hex($randomData); + } + } + // Last resort which you probably should just get rid of: + $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true); + + return substr(hash('sha512', $randomData), 0, 40); + } + + /** + * Generates an unique refresh token + * + * Implementing classes may want to override this function to implement + * other refresh token generation schemes. + * + * @return + * An unique refresh. + * + * @ingroup oauth2_section_4 + * @see OAuth2::generateAccessToken() + */ + protected function generateRefreshToken() + { + return $this->generateAccessToken(); // let's reuse the same scheme for token generation + } + + /** + * Handle the revoking of refresh tokens, and access tokens if supported / desirable + * RFC7009 specifies that "If the server is unable to locate the token using + * the given hint, it MUST extend its search across all of its supported token types" + * + * @param $token + * @param null $tokenTypeHint + * @return boolean + */ + public function revokeToken($token, $tokenTypeHint = null) + { + if ($tokenTypeHint == 'refresh_token') { + if ($this->refreshStorage && $revoked = $this->refreshStorage->unsetRefreshToken($token)) { + return true; + } + } + + /** @TODO remove in v2 */ + if (!method_exists($this->tokenStorage, 'unsetAccessToken')) { + throw new \RuntimeException( + sprintf('Token storage %s must implement unsetAccessToken method', get_class($this->tokenStorage) + )); + } + + $revoked = $this->tokenStorage->unsetAccessToken($token); + + // if a typehint is supplied and fails, try other storages + // @see https://tools.ietf.org/html/rfc7009#section-2.1 + if (!$revoked && $tokenTypeHint != 'refresh_token') { + if ($this->refreshStorage) { + $revoked = $this->refreshStorage->unsetRefreshToken($token); + } + } + + return $revoked; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessTokenInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessTokenInterface.php new file mode 100644 index 0000000..4bd3928 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessTokenInterface.php @@ -0,0 +1,34 @@ + + */ +interface AccessTokenInterface extends ResponseTypeInterface +{ + /** + * Handle the creation of access token, also issue refresh token if supported / desirable. + * + * @param $client_id client identifier related to the access token. + * @param $user_id user ID associated with the access token + * @param $scope OPTONAL scopes to be stored in space-separated string. + * @param bool $includeRefreshToken if true, a new refresh_token will be added to the response + * + * @see http://tools.ietf.org/html/rfc6749#section-5 + * @ingroup oauth2_section_5 + */ + public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true); + + /** + * Handle the revoking of refresh tokens, and access tokens if supported / desirable + * + * @param $token + * @param $tokenTypeHint + * @return mixed + * + * @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x + */ + //public function revokeToken($token, $tokenTypeHint); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCode.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCode.php new file mode 100644 index 0000000..6a305fd --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCode.php @@ -0,0 +1,100 @@ + + */ +class AuthorizationCode implements AuthorizationCodeInterface +{ + protected $storage; + protected $config; + + public function __construct(AuthorizationCodeStorageInterface $storage, array $config = array()) + { + $this->storage = $storage; + $this->config = array_merge(array( + 'enforce_redirect' => false, + 'auth_code_lifetime' => 30, + ), $config); + } + + public function getAuthorizeResponse($params, $user_id = null) + { + // build the URL to redirect to + $result = array('query' => array()); + + $params += array('scope' => null, 'state' => null); + + $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope']); + + if (isset($params['state'])) { + $result['query']['state'] = $params['state']; + } + + return array($params['redirect_uri'], $result); + } + + /** + * Handle the creation of the authorization code. + * + * @param $client_id + * Client identifier related to the authorization code + * @param $user_id + * User ID associated with the authorization code + * @param $redirect_uri + * An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @ingroup oauth2_section_4 + */ + public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null) + { + $code = $this->generateAuthorizationCode(); + $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope); + + return $code; + } + + /** + * @return + * TRUE if the grant type requires a redirect_uri, FALSE if not + */ + public function enforceRedirect() + { + return $this->config['enforce_redirect']; + } + + /** + * Generates an unique auth code. + * + * Implementing classes may want to override this function to implement + * other auth code generation schemes. + * + * @return + * An unique auth code. + * + * @ingroup oauth2_section_4 + */ + protected function generateAuthorizationCode() + { + $tokenLen = 40; + if (function_exists('mcrypt_create_iv')) { + $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM); + } elseif (function_exists('openssl_random_pseudo_bytes')) { + $randomData = openssl_random_pseudo_bytes(100); + } elseif (@file_exists('/dev/urandom')) { // Get 100 bytes of random data + $randomData = file_get_contents('/dev/urandom', false, null, 0, 100) . uniqid(mt_rand(), true); + } else { + $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true); + } + + return substr(hash('sha512', $randomData), 0, $tokenLen); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCodeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCodeInterface.php new file mode 100644 index 0000000..df777e2 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCodeInterface.php @@ -0,0 +1,30 @@ + + */ +interface AuthorizationCodeInterface extends ResponseTypeInterface +{ + /** + * @return + * TRUE if the grant type requires a redirect_uri, FALSE if not + */ + public function enforceRedirect(); + + /** + * Handle the creation of the authorization code. + * + * @param $client_id client identifier related to the authorization code + * @param $user_id user id associated with the authorization code + * @param $redirect_uri an absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param $scope OPTIONAL scopes to be stored in space-separated string. + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @ingroup oauth2_section_4 + */ + public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/JwtAccessToken.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/JwtAccessToken.php new file mode 100644 index 0000000..3942fe4 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/JwtAccessToken.php @@ -0,0 +1,124 @@ + + */ +class JwtAccessToken extends AccessToken +{ + protected $publicKeyStorage; + protected $encryptionUtil; + + /** + * @param $config + * - store_encrypted_token_string (bool true) + * whether the entire encrypted string is stored, + * or just the token ID is stored + */ + public function __construct(PublicKeyInterface $publicKeyStorage = null, AccessTokenStorageInterface $tokenStorage = null, RefreshTokenInterface $refreshStorage = null, array $config = array(), EncryptionInterface $encryptionUtil = null) + { + $this->publicKeyStorage = $publicKeyStorage; + $config = array_merge(array( + 'store_encrypted_token_string' => true, + 'issuer' => '' + ), $config); + if (is_null($tokenStorage)) { + // a pass-thru, so we can call the parent constructor + $tokenStorage = new Memory(); + } + if (is_null($encryptionUtil)) { + $encryptionUtil = new Jwt(); + } + $this->encryptionUtil = $encryptionUtil; + parent::__construct($tokenStorage, $refreshStorage, $config); + } + + /** + * Handle the creation of access token, also issue refresh token if supported / desirable. + * + * @param $client_id + * Client identifier related to the access token. + * @param $user_id + * User ID associated with the access token + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * @param bool $includeRefreshToken + * If true, a new refresh_token will be added to the response + * + * @see http://tools.ietf.org/html/rfc6749#section-5 + * @ingroup oauth2_section_5 + */ + public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true) + { + // token to encrypt + $expires = time() + $this->config['access_lifetime']; + $id = $this->generateAccessToken(); + $jwtAccessToken = array( + 'id' => $id, // for BC (see #591) + 'jti' => $id, + 'iss' => $this->config['issuer'], + 'aud' => $client_id, + 'sub' => $user_id, + 'exp' => $expires, + 'iat' => time(), + 'token_type' => $this->config['token_type'], + 'scope' => $scope + ); + + /* + * Encode the token data into a single access_token string + */ + $access_token = $this->encodeToken($jwtAccessToken, $client_id); + + /* + * Save the token to a secondary storage. This is implemented on the + * OAuth2\Storage\JwtAccessToken side, and will not actually store anything, + * if no secondary storage has been supplied + */ + $token_to_store = $this->config['store_encrypted_token_string'] ? $access_token : $jwtAccessToken['id']; + $this->tokenStorage->setAccessToken($token_to_store, $client_id, $user_id, $this->config['access_lifetime'] ? time() + $this->config['access_lifetime'] : null, $scope); + + // token to return to the client + $token = array( + 'access_token' => $access_token, + 'expires_in' => $this->config['access_lifetime'], + 'token_type' => $this->config['token_type'], + 'scope' => $scope + ); + + /* + * Issue a refresh token also, if we support them + * + * Refresh Tokens are considered supported if an instance of OAuth2\Storage\RefreshTokenInterface + * is supplied in the constructor + */ + if ($includeRefreshToken && $this->refreshStorage) { + $refresh_token = $this->generateRefreshToken(); + $expires = 0; + if ($this->config['refresh_token_lifetime'] > 0) { + $expires = time() + $this->config['refresh_token_lifetime']; + } + $this->refreshStorage->setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope); + $token['refresh_token'] = $refresh_token; + } + + return $token; + } + + protected function encodeToken(array $token, $client_id = null) + { + $private_key = $this->publicKeyStorage->getPrivateKey($client_id); + $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); + + return $this->encryptionUtil->encode($token, $private_key, $algorithm); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/ResponseTypeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/ResponseTypeInterface.php new file mode 100644 index 0000000..f8e26a5 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/ResponseTypeInterface.php @@ -0,0 +1,8 @@ +storage = $storage; + } + + /** + * Check if everything in required scope is contained in available scope. + * + * @param $required_scope + * A space-separated string of scopes. + * + * @return + * TRUE if everything in required scope is contained in available scope, + * and FALSE if it isn't. + * + * @see http://tools.ietf.org/html/rfc6749#section-7 + * + * @ingroup oauth2_section_7 + */ + public function checkScope($required_scope, $available_scope) + { + $required_scope = explode(' ', trim($required_scope)); + $available_scope = explode(' ', trim($available_scope)); + + return (count(array_diff($required_scope, $available_scope)) == 0); + } + + /** + * Check if the provided scope exists in storage. + * + * @param $scope + * A space-separated string of scopes. + * + * @return + * TRUE if it exists, FALSE otherwise. + */ + public function scopeExists($scope) + { + // Check reserved scopes first. + $scope = explode(' ', trim($scope)); + $reservedScope = $this->getReservedScopes(); + $nonReservedScopes = array_diff($scope, $reservedScope); + if (count($nonReservedScopes) == 0) { + return true; + } else { + // Check the storage for non-reserved scopes. + $nonReservedScopes = implode(' ', $nonReservedScopes); + + return $this->storage->scopeExists($nonReservedScopes); + } + } + + public function getScopeFromRequest(RequestInterface $request) + { + // "scope" is valid if passed in either POST or QUERY + return $request->request('scope', $request->query('scope')); + } + + public function getDefaultScope($client_id = null) + { + return $this->storage->getDefaultScope($client_id); + } + + /** + * Get reserved scopes needed by the server. + * + * In case OpenID Connect is used, these scopes must include: + * 'openid', offline_access'. + * + * @return + * An array of reserved scopes. + */ + public function getReservedScopes() + { + return array('openid', 'offline_access'); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ScopeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ScopeInterface.php new file mode 100644 index 0000000..5b60f9a --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ScopeInterface.php @@ -0,0 +1,40 @@ + 'OAuth2\Storage\AccessTokenInterface', + 'authorization_code' => 'OAuth2\Storage\AuthorizationCodeInterface', + 'client_credentials' => 'OAuth2\Storage\ClientCredentialsInterface', + 'client' => 'OAuth2\Storage\ClientInterface', + 'refresh_token' => 'OAuth2\Storage\RefreshTokenInterface', + 'user_credentials' => 'OAuth2\Storage\UserCredentialsInterface', + 'user_claims' => 'OAuth2\OpenID\Storage\UserClaimsInterface', + 'public_key' => 'OAuth2\Storage\PublicKeyInterface', + 'jwt_bearer' => 'OAuth2\Storage\JWTBearerInterface', + 'scope' => 'OAuth2\Storage\ScopeInterface', + ); + + protected $responseTypeMap = array( + 'token' => 'OAuth2\ResponseType\AccessTokenInterface', + 'code' => 'OAuth2\ResponseType\AuthorizationCodeInterface', + 'id_token' => 'OAuth2\OpenID\ResponseType\IdTokenInterface', + 'id_token token' => 'OAuth2\OpenID\ResponseType\IdTokenTokenInterface', + 'code id_token' => 'OAuth2\OpenID\ResponseType\CodeIdTokenInterface', + ); + + /** + * @param mixed $storage (array or OAuth2\Storage) - single object or array of objects implementing the + * required storage types (ClientCredentialsInterface and AccessTokenInterface as a minimum) + * @param array $config specify a different token lifetime, token header name, etc + * @param array $grantTypes An array of OAuth2\GrantType\GrantTypeInterface to use for granting access tokens + * @param array $responseTypes Response types to use. array keys should be "code" and and "token" for + * Access Token and Authorization Code response types + * @param OAuth2\TokenType\TokenTypeInterface $tokenType The token type object to use. Valid token types are "bearer" and "mac" + * @param OAuth2\ScopeInterface $scopeUtil The scope utility class to use to validate scope + * @param OAuth2\ClientAssertionType\ClientAssertionTypeInterface $clientAssertionType The method in which to verify the client identity. Default is HttpBasic + * + * @ingroup oauth2_section_7 + */ + public function __construct($storage = array(), array $config = array(), array $grantTypes = array(), array $responseTypes = array(), TokenTypeInterface $tokenType = null, ScopeInterface $scopeUtil = null, ClientAssertionTypeInterface $clientAssertionType = null) + { + $storage = is_array($storage) ? $storage : array($storage); + $this->storages = array(); + foreach ($storage as $key => $service) { + $this->addStorage($service, $key); + } + + // merge all config values. These get passed to our controller objects + $this->config = array_merge(array( + 'use_jwt_access_tokens' => false, + 'store_encrypted_token_string' => true, + 'use_openid_connect' => false, + 'id_lifetime' => 3600, + 'access_lifetime' => 3600, + 'www_realm' => 'Service', + 'token_param_name' => 'access_token', + 'token_bearer_header_name' => 'Bearer', + 'enforce_state' => true, + 'require_exact_redirect_uri' => true, + 'allow_implicit' => false, + 'allow_credentials_in_request_body' => true, + 'allow_public_clients' => true, + 'always_issue_new_refresh_token' => false, + 'unset_refresh_token_after_use' => true, + ), $config); + + foreach ($grantTypes as $key => $grantType) { + $this->addGrantType($grantType, $key); + } + + foreach ($responseTypes as $key => $responseType) { + $this->addResponseType($responseType, $key); + } + + $this->tokenType = $tokenType; + $this->scopeUtil = $scopeUtil; + $this->clientAssertionType = $clientAssertionType; + + if ($this->config['use_openid_connect']) { + $this->validateOpenIdConnect(); + } + } + + public function getAuthorizeController() + { + if (is_null($this->authorizeController)) { + $this->authorizeController = $this->createDefaultAuthorizeController(); + } + + return $this->authorizeController; + } + + public function getTokenController() + { + if (is_null($this->tokenController)) { + $this->tokenController = $this->createDefaultTokenController(); + } + + return $this->tokenController; + } + + public function getResourceController() + { + if (is_null($this->resourceController)) { + $this->resourceController = $this->createDefaultResourceController(); + } + + return $this->resourceController; + } + + public function getUserInfoController() + { + if (is_null($this->userInfoController)) { + $this->userInfoController = $this->createDefaultUserInfoController(); + } + + return $this->userInfoController; + } + + /** + * every getter deserves a setter + */ + public function setAuthorizeController(AuthorizeControllerInterface $authorizeController) + { + $this->authorizeController = $authorizeController; + } + + /** + * every getter deserves a setter + */ + public function setTokenController(TokenControllerInterface $tokenController) + { + $this->tokenController = $tokenController; + } + + /** + * every getter deserves a setter + */ + public function setResourceController(ResourceControllerInterface $resourceController) + { + $this->resourceController = $resourceController; + } + + /** + * every getter deserves a setter + */ + public function setUserInfoController(UserInfoControllerInterface $userInfoController) + { + $this->userInfoController = $userInfoController; + } + + /** + * Return claims about the authenticated end-user. + * This would be called from the "/UserInfo" endpoint as defined in the spec. + * + * @param $request - OAuth2\RequestInterface + * Request object to grant access token + * + * @param $response - OAuth2\ResponseInterface + * Response object containing error messages (failure) or user claims (success) + * + * @throws InvalidArgumentException + * @throws LogicException + * + * @see http://openid.net/specs/openid-connect-core-1_0.html#UserInfo + */ + public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $this->getUserInfoController()->handleUserInfoRequest($request, $this->response); + + return $this->response; + } + + /** + * Grant or deny a requested access token. + * This would be called from the "/token" endpoint as defined in the spec. + * Obviously, you can call your endpoint whatever you want. + * + * @param $request - OAuth2\RequestInterface + * Request object to grant access token + * + * @param $response - OAuth2\ResponseInterface + * Response object containing error messages (failure) or access token (success) + * + * @throws InvalidArgumentException + * @throws LogicException + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @see http://tools.ietf.org/html/rfc6749#section-10.6 + * @see http://tools.ietf.org/html/rfc6749#section-4.1.3 + * + * @ingroup oauth2_section_4 + */ + public function handleTokenRequest(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $this->getTokenController()->handleTokenRequest($request, $this->response); + + return $this->response; + } + + public function grantAccessToken(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $value = $this->getTokenController()->grantAccessToken($request, $this->response); + + return $value; + } + + /** + * Handle a revoke token request + * This would be called from the "/revoke" endpoint as defined in the draft Token Revocation spec + * + * @see https://tools.ietf.org/html/rfc7009#section-2 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return Response|ResponseInterface + */ + public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $this->getTokenController()->handleRevokeRequest($request, $this->response); + + return $this->response; + } + + /** + * Redirect the user appropriately after approval. + * + * After the user has approved or denied the resource request the + * authorization server should call this function to redirect the user + * appropriately. + * + * @param $request + * The request should have the follow parameters set in the querystring: + * - response_type: The requested response: an access token, an + * authorization code, or both. + * - client_id: The client identifier as described in Section 2. + * - redirect_uri: An absolute URI to which the authorization server + * will redirect the user-agent to when the end-user authorization + * step is completed. + * - scope: (optional) The scope of the resource request expressed as a + * list of space-delimited strings. + * - state: (optional) An opaque value used by the client to maintain + * state between the request and callback. + * @param $is_authorized + * TRUE or FALSE depending on whether the user authorized the access. + * @param $user_id + * Identifier of user who authorized the client + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * + * @ingroup oauth2_section_4 + */ + public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null) + { + $this->response = $response; + $this->getAuthorizeController()->handleAuthorizeRequest($request, $this->response, $is_authorized, $user_id); + + return $this->response; + } + + /** + * Pull the authorization request data out of the HTTP request. + * - The redirect_uri is OPTIONAL as per draft 20. But your implementation can enforce it + * by setting $config['enforce_redirect'] to true. + * - The state is OPTIONAL but recommended to enforce CSRF. Draft 21 states, however, that + * CSRF protection is MANDATORY. You can enforce this by setting the $config['enforce_state'] to true. + * + * The draft specifies that the parameters should be retrieved from GET, override the Response + * object to change this + * + * @return + * The authorization parameters so the authorization server can prompt + * the user for approval if valid. + * + * @see http://tools.ietf.org/html/rfc6749#section-4.1.1 + * @see http://tools.ietf.org/html/rfc6749#section-10.12 + * + * @ingroup oauth2_section_3 + */ + public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $value = $this->getAuthorizeController()->validateAuthorizeRequest($request, $this->response); + + return $value; + } + + public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response = null, $scope = null) + { + $this->response = is_null($response) ? new Response() : $response; + $value = $this->getResourceController()->verifyResourceRequest($request, $this->response, $scope); + + return $value; + } + + public function getAccessTokenData(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $value = $this->getResourceController()->getAccessTokenData($request, $this->response); + + return $value; + } + + public function addGrantType(GrantTypeInterface $grantType, $identifier = null) + { + if (!is_string($identifier)) { + $identifier = $grantType->getQuerystringIdentifier(); + } + + $this->grantTypes[$identifier] = $grantType; + + // persist added grant type down to TokenController + if (!is_null($this->tokenController)) { + $this->getTokenController()->addGrantType($grantType, $identifier); + } + } + + /** + * Set a storage object for the server + * + * @param $storage + * An object implementing one of the Storage interfaces + * @param $key + * If null, the storage is set to the key of each storage interface it implements + * + * @see storageMap + */ + public function addStorage($storage, $key = null) + { + // if explicitly set to a valid key, do not "magically" set below + if (isset($this->storageMap[$key])) { + if (!is_null($storage) && !$storage instanceof $this->storageMap[$key]) { + throw new \InvalidArgumentException(sprintf('storage of type "%s" must implement interface "%s"', $key, $this->storageMap[$key])); + } + $this->storages[$key] = $storage; + + // special logic to handle "client" and "client_credentials" strangeness + if ($key === 'client' && !isset($this->storages['client_credentials'])) { + if ($storage instanceof \OAuth2\Storage\ClientCredentialsInterface) { + $this->storages['client_credentials'] = $storage; + } + } elseif ($key === 'client_credentials' && !isset($this->storages['client'])) { + if ($storage instanceof \OAuth2\Storage\ClientInterface) { + $this->storages['client'] = $storage; + } + } + } elseif (!is_null($key) && !is_numeric($key)) { + throw new \InvalidArgumentException(sprintf('unknown storage key "%s", must be one of [%s]', $key, implode(', ', array_keys($this->storageMap)))); + } else { + $set = false; + foreach ($this->storageMap as $type => $interface) { + if ($storage instanceof $interface) { + $this->storages[$type] = $storage; + $set = true; + } + } + + if (!$set) { + throw new \InvalidArgumentException(sprintf('storage of class "%s" must implement one of [%s]', get_class($storage), implode(', ', $this->storageMap))); + } + } + } + + public function addResponseType(ResponseTypeInterface $responseType, $key = null) + { + $key = $this->normalizeResponseType($key); + + if (isset($this->responseTypeMap[$key])) { + if (!$responseType instanceof $this->responseTypeMap[$key]) { + throw new \InvalidArgumentException(sprintf('responseType of type "%s" must implement interface "%s"', $key, $this->responseTypeMap[$key])); + } + $this->responseTypes[$key] = $responseType; + } elseif (!is_null($key) && !is_numeric($key)) { + throw new \InvalidArgumentException(sprintf('unknown responseType key "%s", must be one of [%s]', $key, implode(', ', array_keys($this->responseTypeMap)))); + } else { + $set = false; + foreach ($this->responseTypeMap as $type => $interface) { + if ($responseType instanceof $interface) { + $this->responseTypes[$type] = $responseType; + $set = true; + } + } + + if (!$set) { + throw new \InvalidArgumentException(sprintf('Unknown response type %s. Please implement one of [%s]', get_class($responseType), implode(', ', $this->responseTypeMap))); + } + } + } + + public function getScopeUtil() + { + if (!$this->scopeUtil) { + $storage = isset($this->storages['scope']) ? $this->storages['scope'] : null; + $this->scopeUtil = new Scope($storage); + } + + return $this->scopeUtil; + } + + /** + * every getter deserves a setter + */ + public function setScopeUtil($scopeUtil) + { + $this->scopeUtil = $scopeUtil; + } + + protected function createDefaultAuthorizeController() + { + if (!isset($this->storages['client'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\ClientInterface to use the authorize server"); + } + if (0 == count($this->responseTypes)) { + $this->responseTypes = $this->getDefaultResponseTypes(); + } + if ($this->config['use_openid_connect'] && !isset($this->responseTypes['id_token'])) { + $this->responseTypes['id_token'] = $this->createDefaultIdTokenResponseType(); + if ($this->config['allow_implicit']) { + $this->responseTypes['id_token token'] = $this->createDefaultIdTokenTokenResponseType(); + } + } + + $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_implicit enforce_state require_exact_redirect_uri'))); + + if ($this->config['use_openid_connect']) { + return new OpenIDAuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil()); + } + + return new AuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil()); + } + + protected function createDefaultTokenController() + { + if (0 == count($this->grantTypes)) { + $this->grantTypes = $this->getDefaultGrantTypes(); + } + + if (is_null($this->clientAssertionType)) { + // see if HttpBasic assertion type is requred. If so, then create it from storage classes. + foreach ($this->grantTypes as $grantType) { + if (!$grantType instanceof ClientAssertionTypeInterface) { + if (!isset($this->storages['client_credentials'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\ClientCredentialsInterface to use the token server"); + } + $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_credentials_in_request_body allow_public_clients'))); + $this->clientAssertionType = new HttpBasic($this->storages['client_credentials'], $config); + break; + } + } + } + + if (!isset($this->storages['client'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\ClientInterface to use the token server"); + } + + $accessTokenResponseType = $this->getAccessTokenResponseType(); + + return new TokenController($accessTokenResponseType, $this->storages['client'], $this->grantTypes, $this->clientAssertionType, $this->getScopeUtil()); + } + + protected function createDefaultResourceController() + { + if ($this->config['use_jwt_access_tokens']) { + // overwrites access token storage with crypto token storage if "use_jwt_access_tokens" is set + if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof JwtAccessTokenInterface) { + $this->storages['access_token'] = $this->createDefaultJwtAccessTokenStorage(); + } + } elseif (!isset($this->storages['access_token'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use JwtAccessTokens to use the resource server"); + } + + if (!$this->tokenType) { + $this->tokenType = $this->getDefaultTokenType(); + } + + $config = array_intersect_key($this->config, array('www_realm' => '')); + + return new ResourceController($this->tokenType, $this->storages['access_token'], $config, $this->getScopeUtil()); + } + + protected function createDefaultUserInfoController() + { + if ($this->config['use_jwt_access_tokens']) { + // overwrites access token storage with crypto token storage if "use_jwt_access_tokens" is set + if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof JwtAccessTokenInterface) { + $this->storages['access_token'] = $this->createDefaultJwtAccessTokenStorage(); + } + } elseif (!isset($this->storages['access_token'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use JwtAccessTokens to use the UserInfo server"); + } + + if (!isset($this->storages['user_claims'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use the UserInfo server"); + } + + if (!$this->tokenType) { + $this->tokenType = $this->getDefaultTokenType(); + } + + $config = array_intersect_key($this->config, array('www_realm' => '')); + + return new UserInfoController($this->tokenType, $this->storages['access_token'], $this->storages['user_claims'], $config, $this->getScopeUtil()); + } + + protected function getDefaultTokenType() + { + $config = array_intersect_key($this->config, array_flip(explode(' ', 'token_param_name token_bearer_header_name'))); + + return new Bearer($config); + } + + protected function getDefaultResponseTypes() + { + $responseTypes = array(); + + if ($this->config['allow_implicit']) { + $responseTypes['token'] = $this->getAccessTokenResponseType(); + } + + if ($this->config['use_openid_connect']) { + $responseTypes['id_token'] = $this->getIdTokenResponseType(); + if ($this->config['allow_implicit']) { + $responseTypes['id_token token'] = $this->getIdTokenTokenResponseType(); + } + } + + if (isset($this->storages['authorization_code'])) { + $config = array_intersect_key($this->config, array_flip(explode(' ', 'enforce_redirect auth_code_lifetime'))); + if ($this->config['use_openid_connect']) { + if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) { + throw new \LogicException("Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when 'use_openid_connect' is true"); + } + $responseTypes['code'] = new OpenIDAuthorizationCodeResponseType($this->storages['authorization_code'], $config); + $responseTypes['code id_token'] = new CodeIdToken($responseTypes['code'], $responseTypes['id_token']); + } else { + $responseTypes['code'] = new AuthorizationCodeResponseType($this->storages['authorization_code'], $config); + } + } + + if (count($responseTypes) == 0) { + throw new \LogicException("You must supply an array of response_types in the constructor or implement a OAuth2\Storage\AuthorizationCodeInterface storage object or set 'allow_implicit' to true and implement a OAuth2\Storage\AccessTokenInterface storage object"); + } + + return $responseTypes; + } + + protected function getDefaultGrantTypes() + { + $grantTypes = array(); + + if (isset($this->storages['user_credentials'])) { + $grantTypes['password'] = new UserCredentials($this->storages['user_credentials']); + } + + if (isset($this->storages['client_credentials'])) { + $config = array_intersect_key($this->config, array('allow_credentials_in_request_body' => '')); + $grantTypes['client_credentials'] = new ClientCredentials($this->storages['client_credentials'], $config); + } + + if (isset($this->storages['refresh_token'])) { + $config = array_intersect_key($this->config, array_flip(explode(' ', 'always_issue_new_refresh_token unset_refresh_token_after_use'))); + $grantTypes['refresh_token'] = new RefreshToken($this->storages['refresh_token'], $config); + } + + if (isset($this->storages['authorization_code'])) { + if ($this->config['use_openid_connect']) { + if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) { + throw new \LogicException("Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when 'use_openid_connect' is true"); + } + $grantTypes['authorization_code'] = new OpenIDAuthorizationCodeGrantType($this->storages['authorization_code']); + } else { + $grantTypes['authorization_code'] = new AuthorizationCode($this->storages['authorization_code']); + } + } + + if (count($grantTypes) == 0) { + throw new \LogicException("Unable to build default grant types - You must supply an array of grant_types in the constructor"); + } + + return $grantTypes; + } + + protected function getAccessTokenResponseType() + { + if (isset($this->responseTypes['token'])) { + return $this->responseTypes['token']; + } + + if ($this->config['use_jwt_access_tokens']) { + return $this->createDefaultJwtAccessTokenResponseType(); + } + + return $this->createDefaultAccessTokenResponseType(); + } + + protected function getIdTokenResponseType() + { + if (isset($this->responseTypes['id_token'])) { + return $this->responseTypes['id_token']; + } + + return $this->createDefaultIdTokenResponseType(); + } + + protected function getIdTokenTokenResponseType() + { + if (isset($this->responseTypes['id_token token'])) { + return $this->responseTypes['id_token token']; + } + + return $this->createDefaultIdTokenTokenResponseType(); + } + + /** + * For Resource Controller + */ + protected function createDefaultJwtAccessTokenStorage() + { + if (!isset($this->storages['public_key'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens"); + } + $tokenStorage = null; + if (!empty($this->config['store_encrypted_token_string']) && isset($this->storages['access_token'])) { + $tokenStorage = $this->storages['access_token']; + } + // wrap the access token storage as required. + return new JwtAccessTokenStorage($this->storages['public_key'], $tokenStorage); + } + + /** + * For Authorize and Token Controllers + */ + protected function createDefaultJwtAccessTokenResponseType() + { + if (!isset($this->storages['public_key'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens"); + } + + $tokenStorage = null; + if (isset($this->storages['access_token'])) { + $tokenStorage = $this->storages['access_token']; + } + + $refreshStorage = null; + if (isset($this->storages['refresh_token'])) { + $refreshStorage = $this->storages['refresh_token']; + } + + $config = array_intersect_key($this->config, array_flip(explode(' ', 'store_encrypted_token_string issuer access_lifetime refresh_token_lifetime'))); + + return new JwtAccessToken($this->storages['public_key'], $tokenStorage, $refreshStorage, $config); + } + + protected function createDefaultAccessTokenResponseType() + { + if (!isset($this->storages['access_token'])) { + throw new \LogicException("You must supply a response type implementing OAuth2\ResponseType\AccessTokenInterface, or a storage object implementing OAuth2\Storage\AccessTokenInterface to use the token server"); + } + + $refreshStorage = null; + if (isset($this->storages['refresh_token'])) { + $refreshStorage = $this->storages['refresh_token']; + } + + $config = array_intersect_key($this->config, array_flip(explode(' ', 'access_lifetime refresh_token_lifetime'))); + $config['token_type'] = $this->tokenType ? $this->tokenType->getTokenType() : $this->getDefaultTokenType()->getTokenType(); + + return new AccessToken($this->storages['access_token'], $refreshStorage, $config); + } + + protected function createDefaultIdTokenResponseType() + { + if (!isset($this->storages['user_claims'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use openid connect"); + } + if (!isset($this->storages['public_key'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use openid connect"); + } + + $config = array_intersect_key($this->config, array_flip(explode(' ', 'issuer id_lifetime'))); + + return new IdToken($this->storages['user_claims'], $this->storages['public_key'], $config); + } + + protected function createDefaultIdTokenTokenResponseType() + { + return new IdTokenToken($this->getAccessTokenResponseType(), $this->getIdTokenResponseType()); + } + + protected function validateOpenIdConnect() + { + $authCodeGrant = $this->getGrantType('authorization_code'); + if (!empty($authCodeGrant) && !$authCodeGrant instanceof OpenIDAuthorizationCodeGrantType) { + throw new \InvalidArgumentException('You have enabled OpenID Connect, but supplied a grant type that does not support it.'); + } + } + + protected function normalizeResponseType($name) + { + // for multiple-valued response types - make them alphabetical + if (!empty($name) && false !== strpos($name, ' ')) { + $types = explode(' ', $name); + sort($types); + $name = implode(' ', $types); + } + + return $name; + } + + public function getResponse() + { + return $this->response; + } + + public function getStorages() + { + return $this->storages; + } + + public function getStorage($name) + { + return isset($this->storages[$name]) ? $this->storages[$name] : null; + } + + public function getGrantTypes() + { + return $this->grantTypes; + } + + public function getGrantType($name) + { + return isset($this->grantTypes[$name]) ? $this->grantTypes[$name] : null; + } + + public function getResponseTypes() + { + return $this->responseTypes; + } + + public function getResponseType($name) + { + // for multiple-valued response types - make them alphabetical + $name = $this->normalizeResponseType($name); + + return isset($this->responseTypes[$name]) ? $this->responseTypes[$name] : null; + } + + public function getTokenType() + { + return $this->tokenType; + } + + public function getClientAssertionType() + { + return $this->clientAssertionType; + } + + public function setConfig($name, $value) + { + $this->config[$name] = $value; + } + + public function getConfig($name, $default = null) + { + return isset($this->config[$name]) ? $this->config[$name] : $default; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AccessTokenInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AccessTokenInterface.php new file mode 100644 index 0000000..ac081bb --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AccessTokenInterface.php @@ -0,0 +1,63 @@ + + */ +interface AccessTokenInterface +{ + /** + * Look up the supplied oauth_token from storage. + * + * We need to retrieve access token data as we create and verify tokens. + * + * @param $oauth_token + * oauth_token to be check with. + * + * @return + * An associative array as below, and return NULL if the supplied oauth_token + * is invalid: + * - expires: Stored expiration in unix timestamp. + * - client_id: (optional) Stored client identifier. + * - user_id: (optional) Stored user identifier. + * - scope: (optional) Stored scope values in space-separated string. + * - id_token: (optional) Stored id_token (if "use_openid_connect" is true). + * + * @ingroup oauth2_section_7 + */ + public function getAccessToken($oauth_token); + + /** + * Store the supplied access token values to storage. + * + * We need to store access token data as we create and verify tokens. + * + * @param $oauth_token oauth_token to be stored. + * @param $client_id client identifier to be stored. + * @param $user_id user identifier to be stored. + * @param int $expires expiration to be stored as a Unix timestamp. + * @param string $scope OPTIONAL Scopes to be stored in space-separated string. + * + * @ingroup oauth2_section_4 + */ + public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null); + + /** + * Expire an access token. + * + * This is not explicitly required in the spec, but if defined in a draft RFC for token + * revoking (RFC 7009) https://tools.ietf.org/html/rfc7009 + * + * @param $access_token + * Access token to be expired. + * + * @ingroup oauth2_section_6 + * + * @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x + */ + //public function unsetAccessToken($access_token); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AuthorizationCodeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AuthorizationCodeInterface.php new file mode 100644 index 0000000..3beb0e4 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AuthorizationCodeInterface.php @@ -0,0 +1,86 @@ + + */ +interface AuthorizationCodeInterface +{ + /** + * The Authorization Code grant type supports a response type of "code". + * + * @var string + * @see http://tools.ietf.org/html/rfc6749#section-1.4.1 + * @see http://tools.ietf.org/html/rfc6749#section-4.2 + */ + const RESPONSE_TYPE_CODE = "code"; + + /** + * Fetch authorization code data (probably the most common grant type). + * + * Retrieve the stored data for the given authorization code. + * + * Required for OAuth2::GRANT_TYPE_AUTH_CODE. + * + * @param $code + * Authorization code to be check with. + * + * @return + * An associative array as below, and NULL if the code is invalid + * @code + * return array( + * "client_id" => CLIENT_ID, // REQUIRED Stored client identifier + * "user_id" => USER_ID, // REQUIRED Stored user identifier + * "expires" => EXPIRES, // REQUIRED Stored expiration in unix timestamp + * "redirect_uri" => REDIRECT_URI, // REQUIRED Stored redirect URI + * "scope" => SCOPE, // OPTIONAL Stored scope values in space-separated string + * ); + * @endcode + * + * @see http://tools.ietf.org/html/rfc6749#section-4.1 + * + * @ingroup oauth2_section_4 + */ + public function getAuthorizationCode($code); + + /** + * Take the provided authorization code values and store them somewhere. + * + * This function should be the storage counterpart to getAuthCode(). + * + * If storage fails for some reason, we're not currently checking for + * any sort of success/failure, so you should bail out of the script + * and provide a descriptive fail message. + * + * Required for OAuth2::GRANT_TYPE_AUTH_CODE. + * + * @param string $code Authorization code to be stored. + * @param mixed $client_id Client identifier to be stored. + * @param mixed $user_id User identifier to be stored. + * @param string $redirect_uri Redirect URI(s) to be stored in a space-separated string. + * @param int $expires Expiration to be stored as a Unix timestamp. + * @param string $scope OPTIONAL Scopes to be stored in space-separated string. + * + * @ingroup oauth2_section_4 + */ + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null); + + /** + * once an Authorization Code is used, it must be exipired + * + * @see http://tools.ietf.org/html/rfc6749#section-4.1.2 + * + * The client MUST NOT use the authorization code + * more than once. If an authorization code is used more than + * once, the authorization server MUST deny the request and SHOULD + * revoke (when possible) all tokens previously issued based on + * that authorization code + * + */ + public function expireAuthorizationCode($code); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Cassandra.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Cassandra.php new file mode 100644 index 0000000..4c661ed --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Cassandra.php @@ -0,0 +1,469 @@ + + * composer require thobbs/phpcassa:dev-master + * + * + * Once this is done, instantiate the + * + * $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_server', array('127.0.0.1:9160')); + * + * + * Then, register the storage client: + * + * $storage = new OAuth2\Storage\Cassandra($cassandra); + * $storage->setClientDetails($client_id, $client_secret, $redirect_uri); + * + * + * @see test/lib/OAuth2/Storage/Bootstrap::getCassandraStorage + */ +class Cassandra implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + PublicKeyInterface, + UserClaimsInterface, + OpenIDAuthorizationCodeInterface +{ + + private $cache; + + /* The cassandra client */ + protected $cassandra; + + /* Configuration array */ + protected $config; + + /** + * Cassandra Storage! uses phpCassa + * + * @param \phpcassa\ConnectionPool $cassandra + * @param array $config + */ + public function __construct($connection = array(), array $config = array()) + { + if ($connection instanceof ConnectionPool) { + $this->cassandra = $connection; + } else { + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Cassandra must be an instance of phpcassa\Connection\ConnectionPool or a configuration array'); + } + $connection = array_merge(array( + 'keyspace' => 'oauth2', + 'servers' => null, + ), $connection); + + $this->cassandra = new ConnectionPool($connection['keyspace'], $connection['servers']); + } + + $this->config = array_merge(array( + // cassandra config + 'column_family' => 'auth', + + // key names + 'client_key' => 'oauth_clients:', + 'access_token_key' => 'oauth_access_tokens:', + 'refresh_token_key' => 'oauth_refresh_tokens:', + 'code_key' => 'oauth_authorization_codes:', + 'user_key' => 'oauth_users:', + 'jwt_key' => 'oauth_jwt:', + 'scope_key' => 'oauth_scopes:', + 'public_key_key' => 'oauth_public_keys:', + ), $config); + } + + protected function getValue($key) + { + if (isset($this->cache[$key])) { + return $this->cache[$key]; + } + $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); + + try { + $value = $cf->get($key, new ColumnSlice("", "")); + $value = array_shift($value); + } catch (\cassandra\NotFoundException $e) { + return false; + } + + return json_decode($value, true); + } + + protected function setValue($key, $value, $expire = 0) + { + $this->cache[$key] = $value; + + $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); + + $str = json_encode($value); + if ($expire > 0) { + try { + $seconds = $expire - time(); + // __data key set as C* requires a field, note: max TTL can only be 630720000 seconds + $cf->insert($key, array('__data' => $str), null, $seconds); + } catch (\Exception $e) { + return false; + } + } else { + try { + // __data key set as C* requires a field + $cf->insert($key, array('__data' => $str)); + } catch (\Exception $e) { + return false; + } + } + + return true; + } + + protected function expireValue($key) + { + unset($this->cache[$key]); + + $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); + try { + // __data key set as C* requires a field + $cf->remove($key, array('__data')); + } catch (\Exception $e) { + return false; + } + + return true; + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + return $this->getValue($this->config['code_key'] . $code); + } + + public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + return $this->setValue( + $this->config['code_key'] . $authorization_code, + compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'), + $expires + ); + } + + public function expireAuthorizationCode($code) + { + $key = $this->config['code_key'] . $code; + unset($this->cache[$key]); + + return $this->expireValue($key); + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == sha1($password); + } + + public function getUserDetails($username) + { + return $this->getUser($username); + } + + public function getUser($username) + { + if (!$userInfo = $this->getValue($this->config['user_key'] . $username)) { + return false; + } + + // the default behavior is to use "username" as the user_id + return array_merge(array( + 'user_id' => $username, + ), $userInfo); + } + + public function setUser($username, $password, $first_name = null, $last_name = null) + { + $password = sha1($password); + + return $this->setValue( + $this->config['user_key'] . $username, + compact('username', 'password', 'first_name', 'last_name') + ); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return isset($client['client_secret']) + && $client['client_secret'] == $client_secret; + } + + public function isPublicClient($client_id) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return empty($result['client_secret']);; + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + return $this->getValue($this->config['client_key'] . $client_id); + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + return $this->setValue( + $this->config['client_key'] . $client_id, + compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id') + ); + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, (array) $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + return $this->getValue($this->config['refresh_token_key'] . $refresh_token); + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['refresh_token_key'] . $refresh_token, + compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + public function unsetRefreshToken($refresh_token) + { + return $this->expireValue($this->config['refresh_token_key'] . $refresh_token); + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + return $this->getValue($this->config['access_token_key'].$access_token); + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['access_token_key'].$access_token, + compact('access_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + public function unsetAccessToken($access_token) + { + return $this->expireValue($this->config['access_token_key'] . $access_token); + } + + /* ScopeInterface */ + public function scopeExists($scope) + { + $scope = explode(' ', $scope); + + $result = $this->getValue($this->config['scope_key'].'supported:global'); + + $supportedScope = explode(' ', (string) $result); + + return (count(array_diff($scope, $supportedScope)) == 0); + } + + public function getDefaultScope($client_id = null) + { + if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'].'default:'.$client_id)) { + $result = $this->getValue($this->config['scope_key'].'default:global'); + } + + return $result; + } + + public function setScope($scope, $client_id = null, $type = 'supported') + { + if (!in_array($type, array('default', 'supported'))) { + throw new \InvalidArgumentException('"$type" must be one of "default", "supported"'); + } + + if (is_null($client_id)) { + $key = $this->config['scope_key'].$type.':global'; + } else { + $key = $this->config['scope_key'].$type.':'.$client_id; + } + + return $this->setValue($key, $scope); + } + + /*JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + if (!$jwt = $this->getValue($this->config['jwt_key'] . $client_id)) { + return false; + } + + if (isset($jwt['subject']) && $jwt['subject'] == $subject ) { + return $jwt['key']; + } + + return null; + } + + public function setClientKey($client_id, $key, $subject = null) + { + return $this->setValue($this->config['jwt_key'] . $client_id, array( + 'key' => $key, + 'subject' => $subject + )); + } + + /*ScopeInterface */ + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs cassandra implementation. + throw new \Exception('getJti() for the Cassandra driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs cassandra implementation. + throw new \Exception('setJti() for the Cassandra driver is currently unimplemented.'); + } + + /* PublicKeyInterface */ + public function getPublicKey($client_id = '') + { + $public_key = $this->getValue($this->config['public_key_key'] . $client_id); + if (is_array($public_key)) { + return $public_key['public_key']; + } + $public_key = $this->getValue($this->config['public_key_key']); + if (is_array($public_key)) { + return $public_key['public_key']; + } + } + + public function getPrivateKey($client_id = '') + { + $public_key = $this->getValue($this->config['public_key_key'] . $client_id); + if (is_array($public_key)) { + return $public_key['private_key']; + } + $public_key = $this->getValue($this->config['public_key_key']); + if (is_array($public_key)) { + return $public_key['private_key']; + } + } + + public function getEncryptionAlgorithm($client_id = null) + { + $public_key = $this->getValue($this->config['public_key_key'] . $client_id); + if (is_array($public_key)) { + return $public_key['encryption_algorithm']; + } + $public_key = $this->getValue($this->config['public_key_key']); + if (is_array($public_key)) { + return $public_key['encryption_algorithm']; + } + + return 'RS256'; + } + + /* UserClaimsInterface */ + public function getUserClaims($user_id, $claims) + { + $userDetails = $this->getUserDetails($user_id); + if (!is_array($userDetails)) { + return false; + } + + $claims = explode(' ', trim($claims)); + $userClaims = array(); + + // for each requested claim, if the user has the claim, set it in the response + $validClaims = explode(' ', self::VALID_CLAIMS); + foreach ($validClaims as $validClaim) { + if (in_array($validClaim, $claims)) { + if ($validClaim == 'address') { + // address is an object with subfields + $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); + } else { + $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); + } + } + } + + return $userClaims; + } + + protected function getUserClaim($claim, $userDetails) + { + $userClaims = array(); + $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); + $claimValues = explode(' ', $claimValuesString); + + foreach ($claimValues as $value) { + if ($value == 'email_verified') { + $userClaims[$value] = $userDetails[$value]=='true' ? true : false; + } else { + $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; + } + } + + return $userClaims; + } + +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientCredentialsInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientCredentialsInterface.php new file mode 100644 index 0000000..3318c69 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientCredentialsInterface.php @@ -0,0 +1,49 @@ + + */ +interface ClientCredentialsInterface extends ClientInterface +{ + + /** + * Make sure that the client credentials is valid. + * + * @param $client_id + * Client identifier to be check with. + * @param $client_secret + * (optional) If a secret is required, check that they've given the right one. + * + * @return + * TRUE if the client credentials are valid, and MUST return FALSE if it isn't. + * @endcode + * + * @see http://tools.ietf.org/html/rfc6749#section-3.1 + * + * @ingroup oauth2_section_3 + */ + public function checkClientCredentials($client_id, $client_secret = null); + + /** + * Determine if the client is a "public" client, and therefore + * does not require passing credentials for certain grant types + * + * @param $client_id + * Client identifier to be check with. + * + * @return + * TRUE if the client is public, and FALSE if it isn't. + * @endcode + * + * @see http://tools.ietf.org/html/rfc6749#section-2.3 + * @see https://github.com/bshaffer/oauth2-server-php/issues/257 + * + * @ingroup oauth2_section_2 + */ + public function isPublicClient($client_id); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientInterface.php new file mode 100644 index 0000000..09a5bff --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientInterface.php @@ -0,0 +1,66 @@ + + */ +interface ClientInterface +{ + /** + * Get client details corresponding client_id. + * + * OAuth says we should store request URIs for each registered client. + * Implement this function to grab the stored URI for a given client id. + * + * @param $client_id + * Client identifier to be check with. + * + * @return array + * Client details. The only mandatory key in the array is "redirect_uri". + * This function MUST return FALSE if the given client does not exist or is + * invalid. "redirect_uri" can be space-delimited to allow for multiple valid uris. + * + * return array( + * "redirect_uri" => REDIRECT_URI, // REQUIRED redirect_uri registered for the client + * "client_id" => CLIENT_ID, // OPTIONAL the client id + * "grant_types" => GRANT_TYPES, // OPTIONAL an array of restricted grant types + * "user_id" => USER_ID, // OPTIONAL the user identifier associated with this client + * "scope" => SCOPE, // OPTIONAL the scopes allowed for this client + * ); + * + * + * @ingroup oauth2_section_4 + */ + public function getClientDetails($client_id); + + /** + * Get the scope associated with this client + * + * @return + * STRING the space-delineated scope list for the specified client_id + */ + public function getClientScope($client_id); + + /** + * Check restricted grant types of corresponding client identifier. + * + * If you want to restrict clients to certain grant types, override this + * function. + * + * @param $client_id + * Client identifier to be check with. + * @param $grant_type + * Grant type to be check with + * + * @return + * TRUE if the grant type is supported by this client identifier, and + * FALSE if it isn't. + * + * @ingroup oauth2_section_4 + */ + public function checkRestrictedGrantType($client_id, $grant_type); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/CouchbaseDB.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/CouchbaseDB.php new file mode 100755 index 0000000..1eb55f0 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/CouchbaseDB.php @@ -0,0 +1,331 @@ + + */ +class CouchbaseDB implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $config; + + public function __construct($connection, $config = array()) + { + if ($connection instanceof \Couchbase) { + $this->db = $connection; + } else { + if (!is_array($connection) || !is_array($connection['servers'])) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\CouchbaseDB must be an instance of Couchbase or a configuration array containing a server array'); + } + + $this->db = new \Couchbase($connection['servers'], (!isset($connection['username'])) ? '' : $connection['username'], (!isset($connection['password'])) ? '' : $connection['password'], $connection['bucket'], false); + } + + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + ), $config); + } + + // Helper function to access couchbase item by type: + protected function getObjectByType($name,$id) + { + return json_decode($this->db->get($this->config[$name].'-'.$id),true); + } + + // Helper function to set couchbase item by type: + protected function setObjectByType($name,$id,$array) + { + $array['type'] = $name; + + return $this->db->set($this->config[$name].'-'.$id,json_encode($array)); + } + + // Helper function to delete couchbase item by type, wait for persist to at least 1 node + protected function deleteObjectByType($name,$id) + { + $this->db->delete($this->config[$name].'-'.$id,"",1); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if ($result = $this->getObjectByType('client_table',$client_id)) { + return $result['client_secret'] == $client_secret; + } + + return false; + } + + public function isPublicClient($client_id) + { + if (!$result = $this->getObjectByType('client_table',$client_id)) { + return false; + } + + return empty($result['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + $result = $this->getObjectByType('client_table',$client_id); + + return is_null($result) ? false : $result; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + if ($this->getClientDetails($client_id)) { + + $this->setObjectByType('client_table',$client_id, array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )); + } else { + $this->setObjectByType('client_table',$client_id, array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )); + } + + return true; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + $token = $this->getObjectByType('access_token_table',$access_token); + + return is_null($token) ? false : $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // if it exists, update it. + if ($this->getAccessToken($access_token)) { + $this->setObjectByType('access_token_table',$access_token, array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )); + } else { + $this->setObjectByType('access_token_table',$access_token, array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )); + } + + return true; + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $code = $this->getObjectByType('code_table',$code); + + return is_null($code) ? false : $code; + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $this->setObjectByType('code_table',$code, array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )); + } else { + $this->setObjectByType('code_table',$code,array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )); + } + + return true; + } + + public function expireAuthorizationCode($code) + { + $this->deleteObjectByType('code_table',$code); + + return true; + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + public function getUserDetails($username) + { + if ($user = $this->getUser($username)) { + $user['user_id'] = $user['username']; + } + + return $user; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $token = $this->getObjectByType('refresh_token_table',$refresh_token); + + return is_null($token) ? false : $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $this->setObjectByType('refresh_token_table',$refresh_token, array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope + )); + + return true; + } + + public function unsetRefreshToken($refresh_token) + { + $this->deleteObjectByType('refresh_token_table',$refresh_token); + + return true; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $password; + } + + public function getUser($username) + { + $result = $this->getObjectByType('user_table',$username); + + return is_null($result) ? false : $result; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + if ($this->getUser($username)) { + $this->setObjectByType('user_table',$username, array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )); + + } else { + $this->setObjectByType('user_table',$username, array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )); + + } + + return true; + } + + public function getClientKey($client_id, $subject) + { + if (!$jwt = $this->getObjectByType('jwt_table',$client_id)) { + return false; + } + + if (isset($jwt['subject']) && $jwt['subject'] == $subject) { + return $jwt['key']; + } + + return false; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs couchbase implementation. + throw new \Exception('getJti() for the Couchbase driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs couchbase implementation. + throw new \Exception('setJti() for the Couchbase driver is currently unimplemented.'); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/DynamoDB.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/DynamoDB.php new file mode 100644 index 0000000..596a31a --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/DynamoDB.php @@ -0,0 +1,528 @@ + + * composer require aws/aws-sdk-php:dev-master + * + * + * Once this is done, instantiate the DynamoDB client + * + * $storage = new OAuth2\Storage\Dynamodb(array("key" => "YOURKEY", "secret" => "YOURSECRET", "region" => "YOURREGION")); + * + * + * Table : + * - oauth_access_tokens (primary hash key : access_token) + * - oauth_authorization_codes (primary hash key : authorization_code) + * - oauth_clients (primary hash key : client_id) + * - oauth_jwt (primary hash key : client_id, primary range key : subject) + * - oauth_public_keys (primary hash key : client_id) + * - oauth_refresh_tokens (primary hash key : refresh_token) + * - oauth_scopes (primary hash key : scope, secondary index : is_default-index hash key is_default) + * - oauth_users (primary hash key : username) + * + * @author Frederic AUGUSTE + */ +class DynamoDB implements + AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + PublicKeyInterface, + UserClaimsInterface, + OpenIDAuthorizationCodeInterface +{ + protected $client; + protected $config; + + public function __construct($connection, $config = array()) + { + if (!($connection instanceof DynamoDbClient)) { + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance a configuration array containt key, secret, region'); + } + if (!array_key_exists("key",$connection) || !array_key_exists("secret",$connection) || !array_key_exists("region",$connection) ) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance a configuration array containt key, secret, region'); + } + $this->client = DynamoDbClient::factory(array( + 'key' => $connection["key"], + 'secret' => $connection["secret"], + 'region' =>$connection["region"] + )); + } else { + $this->client = $connection; + } + + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + 'scope_table' => 'oauth_scopes', + 'public_key_table' => 'oauth_public_keys', + ), $config); + } + + /* OAuth2\Storage\ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['client_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + + return $result->count()==1 && $result["Item"]["client_secret"]["S"] == $client_secret; + } + + public function isPublicClient($client_id) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['client_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + + if ($result->count()==0) { + return false ; + } + + return empty($result["Item"]["client_secret"]); + } + + /* OAuth2\Storage\ClientInterface */ + public function getClientDetails($client_id) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['client_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + if ($result->count()==0) { + return false ; + } + $result = $this->dynamo2array($result); + foreach (array('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id') as $key => $val) { + if (!array_key_exists ($val, $result)) { + $result[$val] = null; + } + } + + return $result; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + $clientData = compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id'); + $clientData = array_filter($clientData, function ($value) { return !is_null($value); }); + + $result = $this->client->putItem(array( + 'TableName' => $this->config['client_table'], + 'Item' => $this->client->formatAttributes($clientData) + )); + + return true; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, (array) $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* OAuth2\Storage\AccessTokenInterface */ + public function getAccessToken($access_token) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['access_token_table'], + "Key" => array('access_token' => array('S' => $access_token)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + if (array_key_exists ('expires', $token)) { + $token['expires'] = strtotime($token['expires']); + } + + return $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + $clientData = compact('access_token', 'client_id', 'user_id', 'expires', 'scope'); + $clientData = array_filter($clientData, function ($value) { return !empty($value); }); + + $result = $this->client->putItem(array( + 'TableName' => $this->config['access_token_table'], + 'Item' => $this->client->formatAttributes($clientData) + )); + + return true; + + } + + public function unsetAccessToken($access_token) + { + $result = $this->client->deleteItem(array( + 'TableName' => $this->config['access_token_table'], + 'Key' => $this->client->formatAttributes(array("access_token" => $access_token)) + )); + + return true; + } + + /* OAuth2\Storage\AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['code_table'], + "Key" => array('authorization_code' => array('S' => $code)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + if (!array_key_exists("id_token", $token )) { + $token['id_token'] = null; + } + $token['expires'] = strtotime($token['expires']); + + return $token; + + } + + public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + $clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'id_token', 'scope'); + $clientData = array_filter($clientData, function ($value) { return !empty($value); }); + + $result = $this->client->putItem(array( + 'TableName' => $this->config['code_table'], + 'Item' => $this->client->formatAttributes($clientData) + )); + + return true; + } + + public function expireAuthorizationCode($code) + { + + $result = $this->client->deleteItem(array( + 'TableName' => $this->config['code_table'], + 'Key' => $this->client->formatAttributes(array("authorization_code" => $code)) + )); + + return true; + } + + /* OAuth2\Storage\UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + public function getUserDetails($username) + { + return $this->getUser($username); + } + + /* UserClaimsInterface */ + public function getUserClaims($user_id, $claims) + { + if (!$userDetails = $this->getUserDetails($user_id)) { + return false; + } + + $claims = explode(' ', trim($claims)); + $userClaims = array(); + + // for each requested claim, if the user has the claim, set it in the response + $validClaims = explode(' ', self::VALID_CLAIMS); + foreach ($validClaims as $validClaim) { + if (in_array($validClaim, $claims)) { + if ($validClaim == 'address') { + // address is an object with subfields + $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); + } else { + $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); + } + } + } + + return $userClaims; + } + + protected function getUserClaim($claim, $userDetails) + { + $userClaims = array(); + $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); + $claimValues = explode(' ', $claimValuesString); + + foreach ($claimValues as $value) { + if ($value == 'email_verified') { + $userClaims[$value] = $userDetails[$value]=='true' ? true : false; + } else { + $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; + } + } + + return $userClaims; + } + + /* OAuth2\Storage\RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['refresh_token_table'], + "Key" => array('refresh_token' => array('S' => $refresh_token)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + $token['expires'] = strtotime($token['expires']); + + return $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + $clientData = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'); + $clientData = array_filter($clientData, function ($value) { return !empty($value); }); + + $result = $this->client->putItem(array( + 'TableName' => $this->config['refresh_token_table'], + 'Item' => $this->client->formatAttributes($clientData) + )); + + return true; + } + + public function unsetRefreshToken($refresh_token) + { + $result = $this->client->deleteItem(array( + 'TableName' => $this->config['refresh_token_table'], + 'Key' => $this->client->formatAttributes(array("refresh_token" => $refresh_token)) + )); + + return true; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == sha1($password); + } + + public function getUser($username) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['user_table'], + "Key" => array('username' => array('S' => $username)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + $token['user_id'] = $username; + + return $token; + } + + public function setUser($username, $password, $first_name = null, $last_name = null) + { + // do not store in plaintext + $password = sha1($password); + + $clientData = compact('username', 'password', 'first_name', 'last_name'); + $clientData = array_filter($clientData, function ($value) { return !is_null($value); }); + + $result = $this->client->putItem(array( + 'TableName' => $this->config['user_table'], + 'Item' => $this->client->formatAttributes($clientData) + )); + + return true; + + } + + /* ScopeInterface */ + public function scopeExists($scope) + { + $scope = explode(' ', $scope); + $scope_query = array(); + $count = 0; + foreach ($scope as $key => $val) { + $result = $this->client->query(array( + 'TableName' => $this->config['scope_table'], + 'Select' => 'COUNT', + 'KeyConditions' => array( + 'scope' => array( + 'AttributeValueList' => array(array('S' => $val)), + 'ComparisonOperator' => 'EQ' + ) + ) + )); + $count += $result['Count']; + } + + return $count == count($scope); + } + + public function getDefaultScope($client_id = null) + { + + $result = $this->client->query(array( + 'TableName' => $this->config['scope_table'], + 'IndexName' => 'is_default-index', + 'Select' => 'ALL_ATTRIBUTES', + 'KeyConditions' => array( + 'is_default' => array( + 'AttributeValueList' => array(array('S' => 'true')), + 'ComparisonOperator' => 'EQ', + ), + ) + )); + $defaultScope = array(); + if ($result->count() > 0) { + $array = $result->toArray(); + foreach ($array["Items"] as $item) { + $defaultScope[] = $item['scope']['S']; + } + + return empty($defaultScope) ? null : implode(' ', $defaultScope); + } + + return null; + } + + /* JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['jwt_table'], + "Key" => array('client_id' => array('S' => $client_id), 'subject' => array('S' => $subject)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + + return $token['public_key']; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expires, $jti) + { + //TODO not use. + } + + public function setJti($client_id, $subject, $audience, $expires, $jti) + { + //TODO not use. + } + + /* PublicKeyInterface */ + public function getPublicKey($client_id = '0') + { + + $result = $this->client->getItem(array( + "TableName"=> $this->config['public_key_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + + return $token['public_key']; + + } + + public function getPrivateKey($client_id = '0') + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['public_key_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + + return $token['private_key']; + } + + public function getEncryptionAlgorithm($client_id = null) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['public_key_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + if ($result->count()==0) { + return 'RS256' ; + } + $token = $this->dynamo2array($result); + + return $token['encryption_algorithm']; + } + + /** + * Transform dynamodb resultset to an array. + * @param $dynamodbResult + * @return $array + */ + private function dynamo2array($dynamodbResult) + { + $result = array(); + foreach ($dynamodbResult["Item"] as $key => $val) { + $result[$key] = $val["S"]; + $result[] = $val["S"]; + } + + return $result; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessToken.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessToken.php new file mode 100644 index 0000000..75b49d3 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessToken.php @@ -0,0 +1,88 @@ + + */ +class JwtAccessToken implements JwtAccessTokenInterface +{ + protected $publicKeyStorage; + protected $tokenStorage; + protected $encryptionUtil; + + /** + * @param OAuth2\Encryption\PublicKeyInterface $publicKeyStorage the public key encryption to use + * @param OAuth2\Storage\AccessTokenInterface $tokenStorage OPTIONAL persist the access token to another storage. This is useful if + * you want to retain access token grant information somewhere, but + * is not necessary when using this grant type. + * @param OAuth2\Encryption\EncryptionInterface $encryptionUtil OPTIONAL class to use for "encode" and "decode" functions. + */ + public function __construct(PublicKeyInterface $publicKeyStorage, AccessTokenInterface $tokenStorage = null, EncryptionInterface $encryptionUtil = null) + { + $this->publicKeyStorage = $publicKeyStorage; + $this->tokenStorage = $tokenStorage; + if (is_null($encryptionUtil)) { + $encryptionUtil = new Jwt; + } + $this->encryptionUtil = $encryptionUtil; + } + + public function getAccessToken($oauth_token) + { + // just decode the token, don't verify + if (!$tokenData = $this->encryptionUtil->decode($oauth_token, null, false)) { + return false; + } + + $client_id = isset($tokenData['aud']) ? $tokenData['aud'] : null; + $public_key = $this->publicKeyStorage->getPublicKey($client_id); + $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); + + // now that we have the client_id, verify the token + if (false === $this->encryptionUtil->decode($oauth_token, $public_key, array($algorithm))) { + return false; + } + + // normalize the JWT claims to the format expected by other components in this library + return $this->convertJwtToOAuth2($tokenData); + } + + public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null) + { + if ($this->tokenStorage) { + return $this->tokenStorage->setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope); + } + } + + public function unsetAccessToken($access_token) + { + if ($this->tokenStorage) { + return $this->tokenStorage->unsetAccessToken($access_token); + } + } + + + // converts a JWT access token into an OAuth2-friendly format + protected function convertJwtToOAuth2($tokenData) + { + $keyMapping = array( + 'aud' => 'client_id', + 'exp' => 'expires', + 'sub' => 'user_id' + ); + + foreach ($keyMapping as $jwtKey => $oauth2Key) { + if (isset($tokenData[$jwtKey])) { + $tokenData[$oauth2Key] = $tokenData[$jwtKey]; + unset($tokenData[$jwtKey]); + } + } + + return $tokenData; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessTokenInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessTokenInterface.php new file mode 100644 index 0000000..3abb2aa --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessTokenInterface.php @@ -0,0 +1,14 @@ + + */ +interface JwtAccessTokenInterface extends AccessTokenInterface +{ + +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtBearerInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtBearerInterface.php new file mode 100644 index 0000000..c83aa72 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtBearerInterface.php @@ -0,0 +1,74 @@ + + */ +interface JwtBearerInterface +{ + /** + * Get the public key associated with a client_id + * + * @param $client_id + * Client identifier to be checked with. + * + * @return + * STRING Return the public key for the client_id if it exists, and MUST return FALSE if it doesn't. + */ + public function getClientKey($client_id, $subject); + + /** + * Get a jti (JSON token identifier) by matching against the client_id, subject, audience and expiration. + * + * @param $client_id + * Client identifier to match. + * + * @param $subject + * The subject to match. + * + * @param $audience + * The audience to match. + * + * @param $expiration + * The expiration of the jti. + * + * @param $jti + * The jti to match. + * + * @return + * An associative array as below, and return NULL if the jti does not exist. + * - issuer: Stored client identifier. + * - subject: Stored subject. + * - audience: Stored audience. + * - expires: Stored expiration in unix timestamp. + * - jti: The stored jti. + */ + public function getJti($client_id, $subject, $audience, $expiration, $jti); + + /** + * Store a used jti so that we can check against it to prevent replay attacks. + * @param $client_id + * Client identifier to insert. + * + * @param $subject + * The subject to insert. + * + * @param $audience + * The audience to insert. + * + * @param $expiration + * The expiration of the jti. + * + * @param $jti + * The jti to insert. + */ + public function setJti($client_id, $subject, $audience, $expiration, $jti); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Memory.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Memory.php new file mode 100644 index 0000000..4f0859d --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Memory.php @@ -0,0 +1,369 @@ + + */ +class Memory implements AuthorizationCodeInterface, + UserCredentialsInterface, + UserClaimsInterface, + AccessTokenInterface, + ClientCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + PublicKeyInterface, + OpenIDAuthorizationCodeInterface +{ + public $authorizationCodes; + public $userCredentials; + public $clientCredentials; + public $refreshTokens; + public $accessTokens; + public $jwt; + public $jti; + public $supportedScopes; + public $defaultScope; + public $keys; + + public function __construct($params = array()) + { + $params = array_merge(array( + 'authorization_codes' => array(), + 'user_credentials' => array(), + 'client_credentials' => array(), + 'refresh_tokens' => array(), + 'access_tokens' => array(), + 'jwt' => array(), + 'jti' => array(), + 'default_scope' => null, + 'supported_scopes' => array(), + 'keys' => array(), + ), $params); + + $this->authorizationCodes = $params['authorization_codes']; + $this->userCredentials = $params['user_credentials']; + $this->clientCredentials = $params['client_credentials']; + $this->refreshTokens = $params['refresh_tokens']; + $this->accessTokens = $params['access_tokens']; + $this->jwt = $params['jwt']; + $this->jti = $params['jti']; + $this->supportedScopes = $params['supported_scopes']; + $this->defaultScope = $params['default_scope']; + $this->keys = $params['keys']; + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + if (!isset($this->authorizationCodes[$code])) { + return false; + } + + return array_merge(array( + 'authorization_code' => $code, + ), $this->authorizationCodes[$code]); + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + $this->authorizationCodes[$code] = compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'); + + return true; + } + + public function setAuthorizationCodes($authorization_codes) + { + $this->authorizationCodes = $authorization_codes; + } + + public function expireAuthorizationCode($code) + { + unset($this->authorizationCodes[$code]); + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + $userDetails = $this->getUserDetails($username); + + return $userDetails && $userDetails['password'] && $userDetails['password'] === $password; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + $this->userCredentials[$username] = array( + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName, + ); + + return true; + } + + public function getUserDetails($username) + { + if (!isset($this->userCredentials[$username])) { + return false; + } + + return array_merge(array( + 'user_id' => $username, + 'password' => null, + 'first_name' => null, + 'last_name' => null, + ), $this->userCredentials[$username]); + } + + /* UserClaimsInterface */ + public function getUserClaims($user_id, $claims) + { + if (!$userDetails = $this->getUserDetails($user_id)) { + return false; + } + + $claims = explode(' ', trim($claims)); + $userClaims = array(); + + // for each requested claim, if the user has the claim, set it in the response + $validClaims = explode(' ', self::VALID_CLAIMS); + foreach ($validClaims as $validClaim) { + if (in_array($validClaim, $claims)) { + if ($validClaim == 'address') { + // address is an object with subfields + $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); + } else { + $userClaims = array_merge($this->getUserClaim($validClaim, $userDetails)); + } + } + } + + return $userClaims; + } + + protected function getUserClaim($claim, $userDetails) + { + $userClaims = array(); + $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); + $claimValues = explode(' ', $claimValuesString); + + foreach ($claimValues as $value) { + $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; + } + + return $userClaims; + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + return isset($this->clientCredentials[$client_id]['client_secret']) && $this->clientCredentials[$client_id]['client_secret'] === $client_secret; + } + + public function isPublicClient($client_id) + { + if (!isset($this->clientCredentials[$client_id])) { + return false; + } + + return empty($this->clientCredentials[$client_id]['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + if (!isset($this->clientCredentials[$client_id])) { + return false; + } + + $clientDetails = array_merge(array( + 'client_id' => $client_id, + 'client_secret' => null, + 'redirect_uri' => null, + 'scope' => null, + ), $this->clientCredentials[$client_id]); + + return $clientDetails; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + if (isset($this->clientCredentials[$client_id]['grant_types'])) { + $grant_types = explode(' ', $this->clientCredentials[$client_id]['grant_types']); + + return in_array($grant_type, $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + $this->clientCredentials[$client_id] = array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + ); + + return true; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + return isset($this->refreshTokens[$refresh_token]) ? $this->refreshTokens[$refresh_token] : false; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $this->refreshTokens[$refresh_token] = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'); + + return true; + } + + public function unsetRefreshToken($refresh_token) + { + unset($this->refreshTokens[$refresh_token]); + } + + public function setRefreshTokens($refresh_tokens) + { + $this->refreshTokens = $refresh_tokens; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + return isset($this->accessTokens[$access_token]) ? $this->accessTokens[$access_token] : false; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null, $id_token = null) + { + $this->accessTokens[$access_token] = compact('access_token', 'client_id', 'user_id', 'expires', 'scope', 'id_token'); + + return true; + } + + public function unsetAccessToken($access_token) + { + unset($this->accessTokens[$access_token]); + } + + public function scopeExists($scope) + { + $scope = explode(' ', trim($scope)); + + return (count(array_diff($scope, $this->supportedScopes)) == 0); + } + + public function getDefaultScope($client_id = null) + { + return $this->defaultScope; + } + + /*JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + if (isset($this->jwt[$client_id])) { + $jwt = $this->jwt[$client_id]; + if ($jwt) { + if ($jwt["subject"] == $subject) { + return $jwt["key"]; + } + } + } + + return false; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expires, $jti) + { + foreach ($this->jti as $storedJti) { + if ($storedJti['issuer'] == $client_id && $storedJti['subject'] == $subject && $storedJti['audience'] == $audience && $storedJti['expires'] == $expires && $storedJti['jti'] == $jti) { + return array( + 'issuer' => $storedJti['issuer'], + 'subject' => $storedJti['subject'], + 'audience' => $storedJti['audience'], + 'expires' => $storedJti['expires'], + 'jti' => $storedJti['jti'] + ); + } + } + + return null; + } + + public function setJti($client_id, $subject, $audience, $expires, $jti) + { + $this->jti[] = array('issuer' => $client_id, 'subject' => $subject, 'audience' => $audience, 'expires' => $expires, 'jti' => $jti); + } + + /*PublicKeyInterface */ + public function getPublicKey($client_id = null) + { + if (isset($this->keys[$client_id])) { + return $this->keys[$client_id]['public_key']; + } + + // use a global encryption pair + if (isset($this->keys['public_key'])) { + return $this->keys['public_key']; + } + + return false; + } + + public function getPrivateKey($client_id = null) + { + if (isset($this->keys[$client_id])) { + return $this->keys[$client_id]['private_key']; + } + + // use a global encryption pair + if (isset($this->keys['private_key'])) { + return $this->keys['private_key']; + } + + return false; + } + + public function getEncryptionAlgorithm($client_id = null) + { + if (isset($this->keys[$client_id]['encryption_algorithm'])) { + return $this->keys[$client_id]['encryption_algorithm']; + } + + // use a global encryption algorithm + if (isset($this->keys['encryption_algorithm'])) { + return $this->keys['encryption_algorithm']; + } + + return 'RS256'; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Mongo.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Mongo.php new file mode 100644 index 0000000..9510447 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Mongo.php @@ -0,0 +1,333 @@ + + */ +class Mongo implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $config; + + public function __construct($connection, $config = array()) + { + if ($connection instanceof \MongoDB) { + $this->db = $connection; + } else { + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Mongo must be an instance of MongoDB or a configuration array'); + } + $server = sprintf('mongodb://%s:%d', $connection['host'], $connection['port']); + $m = new \MongoClient($server); + $this->db = $m->{$connection['database']}; + } + + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + ), $config); + } + + // Helper function to access a MongoDB collection by `type`: + protected function collection($name) + { + return $this->db->{$this->config[$name]}; + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if ($result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return $result['client_secret'] == $client_secret; + } + + return false; + } + + public function isPublicClient($client_id) + { + if (!$result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return false; + } + + return empty($result['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + $result = $this->collection('client_table')->findOne(array('client_id' => $client_id)); + + return is_null($result) ? false : $result; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + if ($this->getClientDetails($client_id)) { + $this->collection('client_table')->update( + array('client_id' => $client_id), + array('$set' => array( + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )) + ); + } else { + $client = array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + ); + $this->collection('client_table')->insert($client); + } + + return true; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + $token = $this->collection('access_token_table')->findOne(array('access_token' => $access_token)); + + return is_null($token) ? false : $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // if it exists, update it. + if ($this->getAccessToken($access_token)) { + $this->collection('access_token_table')->update( + array('access_token' => $access_token), + array('$set' => array( + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )) + ); + } else { + $token = array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + ); + $this->collection('access_token_table')->insert($token); + } + + return true; + } + + public function unsetAccessToken($access_token) + { + $this->collection('access_token_table')->remove(array('access_token' => $access_token)); + } + + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $code = $this->collection('code_table')->findOne(array('authorization_code' => $code)); + + return is_null($code) ? false : $code; + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $this->collection('code_table')->update( + array('authorization_code' => $code), + array('$set' => array( + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )) + ); + } else { + $token = array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + ); + $this->collection('code_table')->insert($token); + } + + return true; + } + + public function expireAuthorizationCode($code) + { + $this->collection('code_table')->remove(array('authorization_code' => $code)); + + return true; + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + public function getUserDetails($username) + { + if ($user = $this->getUser($username)) { + $user['user_id'] = $user['username']; + } + + return $user; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $token = $this->collection('refresh_token_table')->findOne(array('refresh_token' => $refresh_token)); + + return is_null($token) ? false : $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $token = array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope + ); + $this->collection('refresh_token_table')->insert($token); + + return true; + } + + public function unsetRefreshToken($refresh_token) + { + $this->collection('refresh_token_table')->remove(array('refresh_token' => $refresh_token)); + + return true; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $password; + } + + public function getUser($username) + { + $result = $this->collection('user_table')->findOne(array('username' => $username)); + + return is_null($result) ? false : $result; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + if ($this->getUser($username)) { + $this->collection('user_table')->update( + array('username' => $username), + array('$set' => array( + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )) + ); + } else { + $user = array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + ); + $this->collection('user_table')->insert($user); + } + + return true; + } + + public function getClientKey($client_id, $subject) + { + $result = $this->collection('jwt_table')->findOne(array( + 'client_id' => $client_id, + 'subject' => $subject + )); + + return is_null($result) ? false : $result['key']; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('getJti() for the MongoDB driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Pdo.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Pdo.php new file mode 100644 index 0000000..b8fac34 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Pdo.php @@ -0,0 +1,543 @@ + + */ +class Pdo implements + AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + PublicKeyInterface, + UserClaimsInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $config; + + public function __construct($connection, $config = array()) + { + if (!$connection instanceof \PDO) { + if (is_string($connection)) { + $connection = array('dsn' => $connection); + } + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Pdo must be an instance of PDO, a DSN string, or a configuration array'); + } + if (!isset($connection['dsn'])) { + throw new \InvalidArgumentException('configuration array must contain "dsn"'); + } + // merge optional parameters + $connection = array_merge(array( + 'username' => null, + 'password' => null, + 'options' => array(), + ), $connection); + $connection = new \PDO($connection['dsn'], $connection['username'], $connection['password'], $connection['options']); + } + $this->db = $connection; + + // debugging + $connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + 'jti_table' => 'oauth_jti', + 'scope_table' => 'oauth_scopes', + 'public_key_table' => 'oauth_public_keys', + ), $config); + } + + /* OAuth2\Storage\ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); + $stmt->execute(compact('client_id')); + $result = $stmt->fetch(\PDO::FETCH_ASSOC); + + // make this extensible + return $result && $result['client_secret'] == $client_secret; + } + + public function isPublicClient($client_id) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); + $stmt->execute(compact('client_id')); + + if (!$result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return false; + } + + return empty($result['client_secret']); + } + + /* OAuth2\Storage\ClientInterface */ + public function getClientDetails($client_id) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); + $stmt->execute(compact('client_id')); + + return $stmt->fetch(\PDO::FETCH_ASSOC); + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + // if it exists, update it. + if ($this->getClientDetails($client_id)) { + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_secret=:client_secret, redirect_uri=:redirect_uri, grant_types=:grant_types, scope=:scope, user_id=:user_id where client_id=:client_id', $this->config['client_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (client_id, client_secret, redirect_uri, grant_types, scope, user_id) VALUES (:client_id, :client_secret, :redirect_uri, :grant_types, :scope, :user_id)', $this->config['client_table'])); + } + + return $stmt->execute(compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id')); + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, (array) $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* OAuth2\Storage\AccessTokenInterface */ + public function getAccessToken($access_token) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where access_token = :access_token', $this->config['access_token_table'])); + + $token = $stmt->execute(compact('access_token')); + if ($token = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // convert date string back to timestamp + $token['expires'] = strtotime($token['expires']); + } + + return $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + // if it exists, update it. + if ($this->getAccessToken($access_token)) { + $stmt = $this->db->prepare(sprintf('UPDATE %s SET client_id=:client_id, expires=:expires, user_id=:user_id, scope=:scope where access_token=:access_token', $this->config['access_token_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (access_token, client_id, expires, user_id, scope) VALUES (:access_token, :client_id, :expires, :user_id, :scope)', $this->config['access_token_table'])); + } + + return $stmt->execute(compact('access_token', 'client_id', 'user_id', 'expires', 'scope')); + } + + public function unsetAccessToken($access_token) + { + $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE access_token = :access_token', $this->config['access_token_table'])); + + return $stmt->execute(compact('access_token')); + } + + /* OAuth2\Storage\AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where authorization_code = :code', $this->config['code_table'])); + $stmt->execute(compact('code')); + + if ($code = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // convert date string back to timestamp + $code['expires'] = strtotime($code['expires']); + } + + return $code; + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + if (func_num_args() > 6) { + // we are calling with an id token + return call_user_func_array(array($this, 'setAuthorizationCodeWithIdToken'), func_get_args()); + } + + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope where authorization_code=:code', $this->config['code_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope)', $this->config['code_table'])); + } + + return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope')); + } + + private function setAuthorizationCodeWithIdToken($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope, id_token =:id_token where authorization_code=:code', $this->config['code_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope, id_token) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope, :id_token)', $this->config['code_table'])); + } + + return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token')); + } + + public function expireAuthorizationCode($code) + { + $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE authorization_code = :code', $this->config['code_table'])); + + return $stmt->execute(compact('code')); + } + + /* OAuth2\Storage\UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + public function getUserDetails($username) + { + return $this->getUser($username); + } + + /* UserClaimsInterface */ + public function getUserClaims($user_id, $claims) + { + if (!$userDetails = $this->getUserDetails($user_id)) { + return false; + } + + $claims = explode(' ', trim($claims)); + $userClaims = array(); + + // for each requested claim, if the user has the claim, set it in the response + $validClaims = explode(' ', self::VALID_CLAIMS); + foreach ($validClaims as $validClaim) { + if (in_array($validClaim, $claims)) { + if ($validClaim == 'address') { + // address is an object with subfields + $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); + } else { + $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); + } + } + } + + return $userClaims; + } + + protected function getUserClaim($claim, $userDetails) + { + $userClaims = array(); + $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); + $claimValues = explode(' ', $claimValuesString); + + foreach ($claimValues as $value) { + $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; + } + + return $userClaims; + } + + /* OAuth2\Storage\RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $stmt = $this->db->prepare(sprintf('SELECT * FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table'])); + + $token = $stmt->execute(compact('refresh_token')); + if ($token = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // convert expires to epoch time + $token['expires'] = strtotime($token['expires']); + } + + return $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (refresh_token, client_id, user_id, expires, scope) VALUES (:refresh_token, :client_id, :user_id, :expires, :scope)', $this->config['refresh_token_table'])); + + return $stmt->execute(compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope')); + } + + public function unsetRefreshToken($refresh_token) + { + $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table'])); + + return $stmt->execute(compact('refresh_token')); + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == sha1($password); + } + + public function getUser($username) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT * from %s where username=:username', $this->config['user_table'])); + $stmt->execute(array('username' => $username)); + + if (!$userInfo = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return false; + } + + // the default behavior is to use "username" as the user_id + return array_merge(array( + 'user_id' => $username + ), $userInfo); + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + // do not store in plaintext + $password = sha1($password); + + // if it exists, update it. + if ($this->getUser($username)) { + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET password=:password, first_name=:firstName, last_name=:lastName where username=:username', $this->config['user_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (username, password, first_name, last_name) VALUES (:username, :password, :firstName, :lastName)', $this->config['user_table'])); + } + + return $stmt->execute(compact('username', 'password', 'firstName', 'lastName')); + } + + /* ScopeInterface */ + public function scopeExists($scope) + { + $scope = explode(' ', $scope); + $whereIn = implode(',', array_fill(0, count($scope), '?')); + $stmt = $this->db->prepare(sprintf('SELECT count(scope) as count FROM %s WHERE scope IN (%s)', $this->config['scope_table'], $whereIn)); + $stmt->execute($scope); + + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $result['count'] == count($scope); + } + + return false; + } + + public function getDefaultScope($client_id = null) + { + $stmt = $this->db->prepare(sprintf('SELECT scope FROM %s WHERE is_default=:is_default', $this->config['scope_table'])); + $stmt->execute(array('is_default' => true)); + + if ($result = $stmt->fetchAll(\PDO::FETCH_ASSOC)) { + $defaultScope = array_map(function ($row) { + return $row['scope']; + }, $result); + + return implode(' ', $defaultScope); + } + + return null; + } + + /* JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT public_key from %s where client_id=:client_id AND subject=:subject', $this->config['jwt_table'])); + + $stmt->execute(array('client_id' => $client_id, 'subject' => $subject)); + + return $stmt->fetchColumn(); + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expires, $jti) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT * FROM %s WHERE issuer=:client_id AND subject=:subject AND audience=:audience AND expires=:expires AND jti=:jti', $this->config['jti_table'])); + + $stmt->execute(compact('client_id', 'subject', 'audience', 'expires', 'jti')); + + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return array( + 'issuer' => $result['issuer'], + 'subject' => $result['subject'], + 'audience' => $result['audience'], + 'expires' => $result['expires'], + 'jti' => $result['jti'], + ); + } + + return null; + } + + public function setJti($client_id, $subject, $audience, $expires, $jti) + { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (issuer, subject, audience, expires, jti) VALUES (:client_id, :subject, :audience, :expires, :jti)', $this->config['jti_table'])); + + return $stmt->execute(compact('client_id', 'subject', 'audience', 'expires', 'jti')); + } + + /* PublicKeyInterface */ + public function getPublicKey($client_id = null) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT public_key FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); + + $stmt->execute(compact('client_id')); + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $result['public_key']; + } + } + + public function getPrivateKey($client_id = null) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT private_key FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); + + $stmt->execute(compact('client_id')); + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $result['private_key']; + } + } + + public function getEncryptionAlgorithm($client_id = null) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT encryption_algorithm FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); + + $stmt->execute(compact('client_id')); + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $result['encryption_algorithm']; + } + + return 'RS256'; + } + + /** + * DDL to create OAuth2 database and tables for PDO storage + * + * @see https://github.com/dsquier/oauth2-server-php-mysql + */ + public function getBuildSql($dbName = 'oauth2_server_php') + { + $sql = " + CREATE TABLE {$this->config['client_table']} ( + client_id VARCHAR(80) NOT NULL, + client_secret VARCHAR(80) NOT NULL, + redirect_uri VARCHAR(2000), + grant_types VARCHAR(80), + scope VARCHAR(4000), + user_id VARCHAR(80), + PRIMARY KEY (client_id) + ); + + CREATE TABLE {$this->config['access_token_table']} ( + access_token VARCHAR(40) NOT NULL, + client_id VARCHAR(80) NOT NULL, + user_id VARCHAR(80), + expires TIMESTAMP NOT NULL, + scope VARCHAR(4000), + PRIMARY KEY (access_token) + ); + + CREATE TABLE {$this->config['code_table']} ( + authorization_code VARCHAR(40) NOT NULL, + client_id VARCHAR(80) NOT NULL, + user_id VARCHAR(80), + redirect_uri VARCHAR(2000), + expires TIMESTAMP NOT NULL, + scope VARCHAR(4000), + id_token VARCHAR(1000), + PRIMARY KEY (authorization_code) + ); + + CREATE TABLE {$this->config['refresh_token_table']} ( + refresh_token VARCHAR(40) NOT NULL, + client_id VARCHAR(80) NOT NULL, + user_id VARCHAR(80), + expires TIMESTAMP NOT NULL, + scope VARCHAR(4000), + PRIMARY KEY (refresh_token) + ); + + CREATE TABLE {$this->config['user_table']} ( + username VARCHAR(80), + password VARCHAR(80), + first_name VARCHAR(80), + last_name VARCHAR(80), + email VARCHAR(80), + email_verified BOOLEAN, + scope VARCHAR(4000) + ); + + CREATE TABLE {$this->config['scope_table']} ( + scope VARCHAR(80) NOT NULL, + is_default BOOLEAN, + PRIMARY KEY (scope) + ); + + CREATE TABLE {$this->config['jwt_table']} ( + client_id VARCHAR(80) NOT NULL, + subject VARCHAR(80), + public_key VARCHAR(2000) NOT NULL + ); + + CREATE TABLE {$this->config['jti_table']} ( + issuer VARCHAR(80) NOT NULL, + subject VARCHAR(80), + audiance VARCHAR(80), + expires TIMESTAMP NOT NULL, + jti VARCHAR(2000) NOT NULL + ); + + CREATE TABLE {$this->config['public_key_table']} ( + client_id VARCHAR(80), + public_key VARCHAR(2000), + private_key VARCHAR(2000), + encryption_algorithm VARCHAR(100) DEFAULT 'RS256' + ) +"; + + return $sql; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/PublicKeyInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/PublicKeyInterface.php new file mode 100644 index 0000000..108418d --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/PublicKeyInterface.php @@ -0,0 +1,16 @@ + + */ +interface PublicKeyInterface +{ + public function getPublicKey($client_id = null); + public function getPrivateKey($client_id = null); + public function getEncryptionAlgorithm($client_id = null); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Redis.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Redis.php new file mode 100644 index 0000000..c274f91 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Redis.php @@ -0,0 +1,317 @@ + + * $storage = new OAuth2\Storage\Redis($redis); + * $storage->setClientDetails($client_id, $client_secret, $redirect_uri); + * + */ +class Redis implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + OpenIDAuthorizationCodeInterface +{ + + private $cache; + + /* The redis client */ + protected $redis; + + /* Configuration array */ + protected $config; + + /** + * Redis Storage! + * + * @param \Predis\Client $redis + * @param array $config + */ + public function __construct($redis, $config=array()) + { + $this->redis = $redis; + $this->config = array_merge(array( + 'client_key' => 'oauth_clients:', + 'access_token_key' => 'oauth_access_tokens:', + 'refresh_token_key' => 'oauth_refresh_tokens:', + 'code_key' => 'oauth_authorization_codes:', + 'user_key' => 'oauth_users:', + 'jwt_key' => 'oauth_jwt:', + 'scope_key' => 'oauth_scopes:', + ), $config); + } + + protected function getValue($key) + { + if ( isset($this->cache[$key]) ) { + return $this->cache[$key]; + } + $value = $this->redis->get($key); + if ( isset($value) ) { + return json_decode($value, true); + } else { + return false; + } + } + + protected function setValue($key, $value, $expire=0) + { + $this->cache[$key] = $value; + $str = json_encode($value); + if ($expire > 0) { + $seconds = $expire - time(); + $ret = $this->redis->setex($key, $seconds, $str); + } else { + $ret = $this->redis->set($key, $str); + } + + // check that the key was set properly + // if this fails, an exception will usually thrown, so this step isn't strictly necessary + return is_bool($ret) ? $ret : $ret->getPayload() == 'OK'; + } + + protected function expireValue($key) + { + unset($this->cache[$key]); + + return $this->redis->del($key); + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + return $this->getValue($this->config['code_key'] . $code); + } + + public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + return $this->setValue( + $this->config['code_key'] . $authorization_code, + compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'), + $expires + ); + } + + public function expireAuthorizationCode($code) + { + $key = $this->config['code_key'] . $code; + unset($this->cache[$key]); + + return $this->expireValue($key); + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + $user = $this->getUserDetails($username); + + return $user && $user['password'] === $password; + } + + public function getUserDetails($username) + { + return $this->getUser($username); + } + + public function getUser($username) + { + if (!$userInfo = $this->getValue($this->config['user_key'] . $username)) { + return false; + } + + // the default behavior is to use "username" as the user_id + return array_merge(array( + 'user_id' => $username, + ), $userInfo); + } + + public function setUser($username, $password, $first_name = null, $last_name = null) + { + return $this->setValue( + $this->config['user_key'] . $username, + compact('username', 'password', 'first_name', 'last_name') + ); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return isset($client['client_secret']) + && $client['client_secret'] == $client_secret; + } + + public function isPublicClient($client_id) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return empty($result['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + return $this->getValue($this->config['client_key'] . $client_id); + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + return $this->setValue( + $this->config['client_key'] . $client_id, + compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id') + ); + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, (array) $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + return $this->getValue($this->config['refresh_token_key'] . $refresh_token); + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['refresh_token_key'] . $refresh_token, + compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + public function unsetRefreshToken($refresh_token) + { + return $this->expireValue($this->config['refresh_token_key'] . $refresh_token); + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + return $this->getValue($this->config['access_token_key'].$access_token); + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['access_token_key'].$access_token, + compact('access_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + public function unsetAccessToken($access_token) + { + return $this->expireValue($this->config['access_token_key'] . $access_token); + } + + /* ScopeInterface */ + public function scopeExists($scope) + { + $scope = explode(' ', $scope); + + $result = $this->getValue($this->config['scope_key'].'supported:global'); + + $supportedScope = explode(' ', (string) $result); + + return (count(array_diff($scope, $supportedScope)) == 0); + } + + public function getDefaultScope($client_id = null) + { + if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'].'default:'.$client_id)) { + $result = $this->getValue($this->config['scope_key'].'default:global'); + } + + return $result; + } + + public function setScope($scope, $client_id = null, $type = 'supported') + { + if (!in_array($type, array('default', 'supported'))) { + throw new \InvalidArgumentException('"$type" must be one of "default", "supported"'); + } + + if (is_null($client_id)) { + $key = $this->config['scope_key'].$type.':global'; + } else { + $key = $this->config['scope_key'].$type.':'.$client_id; + } + + return $this->setValue($key, $scope); + } + + /*JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + if (!$jwt = $this->getValue($this->config['jwt_key'] . $client_id)) { + return false; + } + + if (isset($jwt['subject']) && $jwt['subject'] == $subject) { + return $jwt['key']; + } + + return null; + } + + public function setClientKey($client_id, $key, $subject = null) + { + return $this->setValue($this->config['jwt_key'] . $client_id, array( + 'key' => $key, + 'subject' => $subject + )); + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs redis implementation. + throw new \Exception('getJti() for the Redis driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs redis implementation. + throw new \Exception('setJti() for the Redis driver is currently unimplemented.'); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/RefreshTokenInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/RefreshTokenInterface.php new file mode 100644 index 0000000..0273f21 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/RefreshTokenInterface.php @@ -0,0 +1,82 @@ + + */ +interface RefreshTokenInterface +{ + /** + * Grant refresh access tokens. + * + * Retrieve the stored data for the given refresh token. + * + * Required for OAuth2::GRANT_TYPE_REFRESH_TOKEN. + * + * @param $refresh_token + * Refresh token to be check with. + * + * @return + * An associative array as below, and NULL if the refresh_token is + * invalid: + * - refresh_token: Refresh token identifier. + * - client_id: Client identifier. + * - user_id: User identifier. + * - expires: Expiration unix timestamp, or 0 if the token doesn't expire. + * - scope: (optional) Scope values in space-separated string. + * + * @see http://tools.ietf.org/html/rfc6749#section-6 + * + * @ingroup oauth2_section_6 + */ + public function getRefreshToken($refresh_token); + + /** + * Take the provided refresh token values and store them somewhere. + * + * This function should be the storage counterpart to getRefreshToken(). + * + * If storage fails for some reason, we're not currently checking for + * any sort of success/failure, so you should bail out of the script + * and provide a descriptive fail message. + * + * Required for OAuth2::GRANT_TYPE_REFRESH_TOKEN. + * + * @param $refresh_token + * Refresh token to be stored. + * @param $client_id + * Client identifier to be stored. + * @param $user_id + * User identifier to be stored. + * @param $expires + * Expiration timestamp to be stored. 0 if the token doesn't expire. + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * + * @ingroup oauth2_section_6 + */ + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null); + + /** + * Expire a used refresh token. + * + * This is not explicitly required in the spec, but is almost implied. + * After granting a new refresh token, the old one is no longer useful and + * so should be forcibly expired in the data store so it can't be used again. + * + * If storage fails for some reason, we're not currently checking for + * any sort of success/failure, so you should bail out of the script + * and provide a descriptive fail message. + * + * @param $refresh_token + * Refresh token to be expirse. + * + * @ingroup oauth2_section_6 + */ + public function unsetRefreshToken($refresh_token); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ScopeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ScopeInterface.php new file mode 100644 index 0000000..a829226 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ScopeInterface.php @@ -0,0 +1,46 @@ + + */ +interface ScopeInterface +{ + /** + * Check if the provided scope exists. + * + * @param $scope + * A space-separated string of scopes. + * + * @return + * TRUE if it exists, FALSE otherwise. + */ + public function scopeExists($scope); + + /** + * The default scope to use in the event the client + * does not request one. By returning "false", a + * request_error is returned by the server to force a + * scope request by the client. By returning "null", + * opt out of requiring scopes + * + * @param $client_id + * An optional client id that can be used to return customized default scopes. + * + * @return + * string representation of default scope, null if + * scopes are not defined, or false to force scope + * request by the client + * + * ex: + * 'default' + * ex: + * null + */ + public function getDefaultScope($client_id = null); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/UserCredentialsInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/UserCredentialsInterface.php new file mode 100644 index 0000000..6e0fd7b --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/UserCredentialsInterface.php @@ -0,0 +1,52 @@ + + */ +interface UserCredentialsInterface +{ + /** + * Grant access tokens for basic user credentials. + * + * Check the supplied username and password for validity. + * + * You can also use the $client_id param to do any checks required based + * on a client, if you need that. + * + * Required for OAuth2::GRANT_TYPE_USER_CREDENTIALS. + * + * @param $username + * Username to be check with. + * @param $password + * Password to be check with. + * + * @return + * TRUE if the username and password are valid, and FALSE if it isn't. + * Moreover, if the username and password are valid, and you want to + * + * @see http://tools.ietf.org/html/rfc6749#section-4.3 + * + * @ingroup oauth2_section_4 + */ + public function checkUserCredentials($username, $password); + + /** + * @return + * ARRAY the associated "user_id" and optional "scope" values + * This function MUST return FALSE if the requested user does not exist or is + * invalid. "scope" is a space-separated list of restricted scopes. + * @code + * return array( + * "user_id" => USER_ID, // REQUIRED user_id to be stored with the authorization code or access token + * "scope" => SCOPE // OPTIONAL space-separated list of restricted scopes + * ); + * @endcode + */ + public function getUserDetails($username); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Bearer.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Bearer.php new file mode 100644 index 0000000..8ac8596 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Bearer.php @@ -0,0 +1,130 @@ +config = array_merge(array( + 'token_param_name' => 'access_token', + 'token_bearer_header_name' => 'Bearer', + ), $config); + } + + public function getTokenType() + { + return 'Bearer'; + } + + /** + * Check if the request has supplied token + * + * @see https://github.com/bshaffer/oauth2-server-php/issues/349#issuecomment-37993588 + */ + public function requestHasToken(RequestInterface $request) + { + $headers = $request->headers('AUTHORIZATION'); + + // check the header, then the querystring, then the request body + return !empty($headers) || (bool) ($request->request($this->config['token_param_name'])) || (bool) ($request->query($this->config['token_param_name'])); + } + + /** + * This is a convenience function that can be used to get the token, which can then + * be passed to getAccessTokenData(). The constraints specified by the draft are + * attempted to be adheared to in this method. + * + * As per the Bearer spec (draft 8, section 2) - there are three ways for a client + * to specify the bearer token, in order of preference: Authorization Header, + * POST and GET. + * + * NB: Resource servers MUST accept tokens via the Authorization scheme + * (http://tools.ietf.org/html/rfc6750#section-2). + * + * @todo Should we enforce TLS/SSL in this function? + * + * @see http://tools.ietf.org/html/rfc6750#section-2.1 + * @see http://tools.ietf.org/html/rfc6750#section-2.2 + * @see http://tools.ietf.org/html/rfc6750#section-2.3 + * + * Old Android version bug (at least with version 2.2) + * @see http://code.google.com/p/android/issues/detail?id=6684 + * + */ + public function getAccessTokenParameter(RequestInterface $request, ResponseInterface $response) + { + $headers = $request->headers('AUTHORIZATION'); + + /** + * Ensure more than one method is not used for including an + * access token + * + * @see http://tools.ietf.org/html/rfc6750#section-3.1 + */ + $methodsUsed = !empty($headers) + (bool) ($request->query($this->config['token_param_name'])) + (bool) ($request->request($this->config['token_param_name'])); + if ($methodsUsed > 1) { + $response->setError(400, 'invalid_request', 'Only one method may be used to authenticate at a time (Auth header, GET or POST)'); + + return null; + } + + /** + * If no authentication is provided, set the status code + * to 401 and return no other error information + * + * @see http://tools.ietf.org/html/rfc6750#section-3.1 + */ + if ($methodsUsed == 0) { + $response->setStatusCode(401); + + return null; + } + + // HEADER: Get the access token from the header + if (!empty($headers)) { + if (!preg_match('/' . $this->config['token_bearer_header_name'] . '\s(\S+)/i', $headers, $matches)) { + $response->setError(400, 'invalid_request', 'Malformed auth header'); + + return null; + } + + return $matches[1]; + } + + if ($request->request($this->config['token_param_name'])) { + // // POST: Get the token from POST data + if (!in_array(strtolower($request->server('REQUEST_METHOD')), array('post', 'put'))) { + $response->setError(400, 'invalid_request', 'When putting the token in the body, the method must be POST or PUT', '#section-2.2'); + + return null; + } + + $contentType = $request->server('CONTENT_TYPE'); + if (false !== $pos = strpos($contentType, ';')) { + $contentType = substr($contentType, 0, $pos); + } + + if ($contentType !== null && $contentType != 'application/x-www-form-urlencoded') { + // IETF specifies content-type. NB: Not all webservers populate this _SERVER variable + // @see http://tools.ietf.org/html/rfc6750#section-2.2 + $response->setError(400, 'invalid_request', 'The content type for POST requests must be "application/x-www-form-urlencoded"'); + + return null; + } + + return $request->request($this->config['token_param_name']); + } + + // GET method + return $request->query($this->config['token_param_name']); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Mac.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Mac.php new file mode 100644 index 0000000..fe6a86a --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Mac.php @@ -0,0 +1,22 @@ +assertTrue(class_exists('OAuth2\Server')); + $this->assertTrue(class_exists('OAuth2\Request')); + $this->assertTrue(class_exists('OAuth2\Response')); + $this->assertTrue(class_exists('OAuth2\GrantType\UserCredentials')); + $this->assertTrue(interface_exists('OAuth2\Storage\AccessTokenInterface')); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/AuthorizeControllerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/AuthorizeControllerTest.php new file mode 100644 index 0000000..c2745d6 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/AuthorizeControllerTest.php @@ -0,0 +1,476 @@ +getTestServer(); + $request = new Request(); + $server->handleAuthorizeRequest($request, $response = new Response(), false); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_client'); + $this->assertEquals($response->getParameter('error_description'), 'No client id supplied'); + } + + public function testInvalidClientIdResponse() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Fake Client ID', // invalid client id + )); + $server->handleAuthorizeRequest($request, $response = new Response(), false); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_client'); + $this->assertEquals($response->getParameter('error_description'), 'The client id supplied is invalid'); + } + + public function testNoRedirectUriSuppliedOrStoredResponse() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + )); + $server->handleAuthorizeRequest($request, $response = new Response(), false); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_uri'); + $this->assertEquals($response->getParameter('error_description'), 'No redirect URI was supplied or stored'); + } + + public function testNoResponseTypeResponse() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + )); + $server->handleAuthorizeRequest($request, $response = new Response(), false); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + parse_str($parts['query'], $query); + + $this->assertEquals($query['error'], 'invalid_request'); + $this->assertEquals($query['error_description'], 'Invalid or missing response type'); + } + + public function testInvalidResponseTypeResponse() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'invalid', // invalid response type + )); + $server->handleAuthorizeRequest($request, $response = new Response(), false); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + parse_str($parts['query'], $query); + + $this->assertEquals($query['error'], 'invalid_request'); + $this->assertEquals($query['error_description'], 'Invalid or missing response type'); + } + + public function testRedirectUriFragmentResponse() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com#fragment', // valid redirect URI + 'response_type' => 'code', // invalid response type + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_uri'); + $this->assertEquals($response->getParameter('error_description'), 'The redirect URI must not contain a fragment'); + } + + public function testEnforceState() + { + $server = $this->getTestServer(array('enforce_state' => true)); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'code', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + parse_str($parts['query'], $query); + + $this->assertEquals($query['error'], 'invalid_request'); + $this->assertEquals($query['error_description'], 'The state parameter is required'); + } + + public function testDoNotEnforceState() + { + $server = $this->getTestServer(array('enforce_state' => false)); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'code', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $this->assertNotContains('error', $response->getHttpHeader('Location')); + } + + public function testEnforceScope() + { + $server = $this->getTestServer(); + $scopeStorage = new Memory(array('default_scope' => false, 'supported_scopes' => array('testscope'))); + $server->setScopeUtil(new Scope($scopeStorage)); + + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'code', + 'state' => 'xyz', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['query'], $query); + + $this->assertEquals($query['error'], 'invalid_client'); + $this->assertEquals($query['error_description'], 'This application requires you specify a scope parameter'); + + $request->query['scope'] = 'testscope'; + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $this->assertNotContains('error', $response->getHttpHeader('Location')); + } + + public function testInvalidRedirectUri() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID with Redirect Uri', // valid client id + 'redirect_uri' => 'http://adobe.com', // invalid redirect URI + 'response_type' => 'code', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'redirect_uri_mismatch'); + $this->assertEquals($response->getParameter('error_description'), 'The redirect URI provided is missing or does not match'); + } + + public function testNoRedirectUriWithMultipleRedirectUris() + { + $server = $this->getTestServer(); + + // create a request with no "redirect_uri" in querystring + $request = new Request(array( + 'client_id' => 'Test Client ID with Multiple Redirect Uris', // valid client id + 'response_type' => 'code', + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_uri'); + $this->assertEquals($response->getParameter('error_description'), 'A redirect URI must be supplied when multiple redirect URIs are registered'); + } + + public function testRedirectUriWithValidRedirectUri() + { + $server = $this->getTestServer(); + + // create a request with no "redirect_uri" in querystring + $request = new Request(array( + 'client_id' => 'Test Client ID with Redirect Uri Parts', // valid client id + 'response_type' => 'code', + 'redirect_uri' => 'http://user:pass@brentertainment.com:2222/authorize/cb?auth_type=oauth&test=true', + 'state' => 'xyz', + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $this->assertContains('code', $response->getHttpHeader('Location')); + } + + public function testRedirectUriWithDifferentQueryAndExactMatchRequired() + { + $server = $this->getTestServer(array('require_exact_redirect_uri' => true)); + + // create a request with no "redirect_uri" in querystring + $request = new Request(array( + 'client_id' => 'Test Client ID with Redirect Uri Parts', // valid client id + 'response_type' => 'code', + 'redirect_uri' => 'http://user:pass@brentertainment.com:2222/authorize/cb?auth_type=oauth&test=true&hereisa=querystring', + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'redirect_uri_mismatch'); + $this->assertEquals($response->getParameter('error_description'), 'The redirect URI provided is missing or does not match'); + } + + public function testRedirectUriWithDifferentQueryAndExactMatchNotRequired() + { + $server = $this->getTestServer(array('require_exact_redirect_uri' => false)); + + // create a request with no "redirect_uri" in querystring + $request = new Request(array( + 'client_id' => 'Test Client ID with Redirect Uri Parts', // valid client id + 'response_type' => 'code', + 'redirect_uri' => 'http://user:pass@brentertainment.com:2222/authorize/cb?auth_type=oauth&test=true&hereisa=querystring', + 'state' => 'xyz', + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $this->assertContains('code', $response->getHttpHeader('Location')); + } + + public function testMultipleRedirectUris() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID with Multiple Redirect Uris', // valid client id + 'redirect_uri' => 'http://brentertainment.com', // valid redirect URI + 'response_type' => 'code', + 'state' => 'xyz' + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + $this->assertEquals($response->getStatusCode(), 302); + $this->assertContains('code', $response->getHttpHeader('Location')); + + // call again with different (but still valid) redirect URI + $request->query['redirect_uri'] = 'http://morehazards.com'; + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + $this->assertEquals($response->getStatusCode(), 302); + $this->assertContains('code', $response->getHttpHeader('Location')); + } + + /** + * @see http://tools.ietf.org/html/rfc6749#section-4.1.3 + * @see https://github.com/bshaffer/oauth2-server-php/issues/163 + */ + public function testNoRedirectUriSuppliedDoesNotRequireTokenRedirectUri() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID with Redirect Uri', // valid client id + 'response_type' => 'code', + 'state' => 'xyz', + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + $this->assertEquals($response->getStatusCode(), 302); + $this->assertContains('state', $response->getHttpHeader('Location')); + $this->assertStringStartsWith('http://brentertainment.com?code=', $response->getHttpHeader('Location')); + + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['query'], $query); + + // call token endpoint with no redirect_uri supplied + $request = TestRequest::createPost(array( + 'client_id' => 'Test Client ID with Redirect Uri', // valid client id + 'client_secret' => 'TestSecret2', + 'grant_type' => 'authorization_code', + 'code' => $query['code'], + )); + + $server->handleTokenRequest($request, $response = new Response(), true); + $this->assertEquals($response->getStatusCode(), 200); + $this->assertNotNull($response->getParameter('access_token')); + } + + public function testUserDeniesAccessResponse() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'code', + 'state' => 'xyz', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), false); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + parse_str($parts['query'], $query); + + $this->assertEquals($query['error'], 'access_denied'); + $this->assertEquals($query['error_description'], 'The user denied access to your application'); + } + + public function testCodeQueryParamIsSet() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'code', + 'state' => 'xyz', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + parse_str($parts['query'], $query); + + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + + $this->assertEquals('http', $parts['scheme']); // same as passed in to redirect_uri + $this->assertEquals('adobe.com', $parts['host']); // same as passed in to redirect_uri + $this->assertArrayHasKey('query', $parts); + $this->assertFalse(isset($parts['fragment'])); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['query'], $query); + $this->assertNotNull($query); + $this->assertArrayHasKey('code', $query); + + // ensure no id_token was saved, since the openid scope wasn't requested + $storage = $server->getStorage('authorization_code'); + $code = $storage->getAuthorizationCode($query['code']); + $this->assertTrue(empty($code['id_token'])); + + // ensure no error was returned + $this->assertFalse(isset($query['error'])); + $this->assertFalse(isset($query['error_description'])); + } + + public function testSuccessfulRequestReturnsStateParameter() + { + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'code', + 'state' => 'test', // valid state string (just needs to be passed back to us) + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + $this->assertArrayHasKey('query', $parts); + parse_str($parts['query'], $query); + + $this->assertArrayHasKey('state', $query); + $this->assertEquals($query['state'], 'test'); + + // ensure no error was returned + $this->assertFalse(isset($query['error'])); + $this->assertFalse(isset($query['error_description'])); + } + + public function testSuccessfulRequestStripsExtraParameters() + { + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'code', + 'state' => 'test', // valid state string (just needs to be passed back to us) + 'fake' => 'something', // extra query param + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $this->assertNotContains('error', $location); + + $parts = parse_url($location); + $this->assertFalse(isset($parts['fake'])); + $this->assertArrayHasKey('query', $parts); + parse_str($parts['query'], $query); + + $this->assertFalse(isset($parmas['fake'])); + $this->assertArrayHasKey('state', $query); + $this->assertEquals($query['state'], 'test'); + } + + public function testSuccessfulOpenidConnectRequest() + { + $server = $this->getTestServer(array( + 'use_openid_connect' => true, + 'issuer' => 'bojanz', + )); + + $request = new Request(array( + 'client_id' => 'Test Client ID', + 'redirect_uri' => 'http://adobe.com', + 'response_type' => 'code', + 'state' => 'xyz', + 'scope' => 'openid', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + parse_str($parts['query'], $query); + + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + $this->assertArrayHasKey('query', $parts); + $this->assertFalse(isset($parts['fragment'])); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['query'], $query); + $this->assertNotNull($query); + $this->assertArrayHasKey('code', $query); + + // ensure no error was returned + $this->assertFalse(isset($query['error'])); + $this->assertFalse(isset($query['error_description'])); + + // confirm that the id_token has been created. + $storage = $server->getStorage('authorization_code'); + $code = $storage->getAuthorizationCode($query['code']); + $this->assertTrue(!empty($code['id_token'])); + } + + public function testCreateController() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $controller = new AuthorizeController($storage); + } + + private function getTestServer($config = array()) + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage, $config); + + // Add the two types supported for authorization grant + $server->addGrantType(new AuthorizationCode($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/ResourceControllerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/ResourceControllerTest.php new file mode 100644 index 0000000..ee6d96f --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/ResourceControllerTest.php @@ -0,0 +1,175 @@ +getTestServer(); + $request = Request::createFromGlobals(); + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 401); + $this->assertNull($response->getParameter('error')); + $this->assertNull($response->getParameter('error_description')); + } + + public function testMalformedHeader() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->headers['AUTHORIZATION'] = 'tH1s i5 B0gU5'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Malformed auth header'); + } + + public function testMultipleTokensSubmitted() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->request['access_token'] = 'TEST'; + $request->query['access_token'] = 'TEST'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Only one method may be used to authenticate at a time (Auth header, GET or POST)'); + } + + public function testInvalidRequestMethod() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->server['REQUEST_METHOD'] = 'GET'; + $request->request['access_token'] = 'TEST'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'When putting the token in the body, the method must be POST or PUT'); + } + + public function testInvalidContentType() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->server['REQUEST_METHOD'] = 'POST'; + $request->server['CONTENT_TYPE'] = 'application/json'; + $request->request['access_token'] = 'TEST'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'The content type for POST requests must be "application/x-www-form-urlencoded"'); + } + + public function testInvalidToken() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->headers['AUTHORIZATION'] = 'Bearer TESTTOKEN'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 401); + $this->assertEquals($response->getParameter('error'), 'invalid_token'); + $this->assertEquals($response->getParameter('error_description'), 'The access token provided is invalid'); + } + + public function testExpiredToken() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-expired'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 401); + $this->assertEquals($response->getParameter('error'), 'expired_token'); + $this->assertEquals($response->getParameter('error_description'), 'The access token provided has expired'); + } + + public function testOutOfScopeToken() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-scope'; + $scope = 'outofscope'; + $allow = $server->verifyResourceRequest($request, $response = new Response(), $scope); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 403); + $this->assertEquals($response->getParameter('error'), 'insufficient_scope'); + $this->assertEquals($response->getParameter('error_description'), 'The request requires higher privileges than provided by the access token'); + + // verify the "scope" has been set in the "WWW-Authenticate" header + preg_match('/scope="(.*?)"/', $response->getHttpHeader('WWW-Authenticate'), $matches); + $this->assertEquals(2, count($matches)); + $this->assertEquals($matches[1], 'outofscope'); + } + + public function testMalformedToken() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-malformed'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 401); + $this->assertEquals($response->getParameter('error'), 'malformed_token'); + $this->assertEquals($response->getParameter('error_description'), 'Malformed token (missing "expires")'); + } + + public function testValidToken() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-scope'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertTrue($allow); + } + + public function testValidTokenWithScopeParam() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-scope'; + $request->query['scope'] = 'testscope'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertTrue($allow); + } + + public function testCreateController() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $tokenType = new \OAuth2\TokenType\Bearer(); + $controller = new ResourceController($tokenType, $storage); + } + + private function getTestServer($config = array()) + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage, $config); + + // Add the two types supported for authorization grant + $server->addGrantType(new AuthorizationCode($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/TokenControllerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/TokenControllerTest.php new file mode 100644 index 0000000..4a217bd --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/TokenControllerTest.php @@ -0,0 +1,289 @@ +getTestServer(); + $server->handleTokenRequest(TestRequest::createPost(), $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'The grant type was not specified in the request'); + } + + public function testInvalidGrantType() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'invalid_grant_type', // invalid grant type + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'unsupported_grant_type'); + $this->assertEquals($response->getParameter('error_description'), 'Grant type "invalid_grant_type" not supported'); + } + + public function testNoClientId() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'code' => 'testcode', + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_client'); + $this->assertEquals($response->getParameter('error_description'), 'Client credentials were not found in the headers or body'); + } + + public function testNoClientSecretWithConfidentialClient() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'code' => 'testcode', + 'client_id' => 'Test Client ID', // valid client id + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_client'); + $this->assertEquals($response->getParameter('error_description'), 'This client is invalid or must authenticate using a client secret'); + } + + public function testNoClientSecretWithEmptySecret() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'code' => 'testcode-empty-secret', + 'client_id' => 'Test Client ID Empty Secret', // valid client id + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 200); + } + + public function testInvalidClientId() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'code' => 'testcode', + 'client_id' => 'Fake Client ID', // invalid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_client'); + $this->assertEquals($response->getParameter('error_description'), 'The client credentials are invalid'); + } + + public function testInvalidClientSecret() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'code' => 'testcode', + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'Fake Client Secret', // invalid client secret + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_client'); + $this->assertEquals($response->getParameter('error_description'), 'The client credentials are invalid'); + } + + public function testValidTokenResponse() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode', // valid authorization code + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertTrue($response instanceof Response); + $this->assertEquals($response->getStatusCode(), 200); + $this->assertNull($response->getParameter('error')); + $this->assertNull($response->getParameter('error_description')); + $this->assertNotNull($response->getParameter('access_token')); + $this->assertNotNull($response->getParameter('expires_in')); + $this->assertNotNull($response->getParameter('token_type')); + } + + public function testValidClientIdScope() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'code' => 'testcode', + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'scope' => 'clientscope1 clientscope2' + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 200); + $this->assertNull($response->getParameter('error')); + $this->assertNull($response->getParameter('error_description')); + $this->assertEquals('clientscope1 clientscope2', $response->getParameter('scope')); + } + + public function testInvalidClientIdScope() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'code' => 'testcode-with-scope', + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'scope' => 'clientscope3' + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'The scope requested is invalid for this request'); + } + + public function testEnforceScope() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage); + $server->addGrantType(new ClientCredentials($storage)); + + $scope = new Scope(array( + 'default_scope' => false, + 'supported_scopes' => array('testscope') + )); + $server->setScopeUtil($scope); + + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $response = $server->handleTokenRequest($request); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'This application requires you specify a scope parameter'); + } + + public function testCanReceiveAccessTokenUsingPasswordGrantTypeWithoutClientSecret() + { + // add the test parameters in memory + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage); + $server->addGrantType(new UserCredentials($storage)); + + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID For Password Grant', // valid client id + 'username' => 'johndoe', // valid username + 'password' => 'password', // valid password for username + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertTrue($response instanceof Response); + $this->assertEquals(200, $response->getStatusCode(), var_export($response, 1)); + $this->assertNull($response->getParameter('error')); + $this->assertNull($response->getParameter('error_description')); + $this->assertNotNull($response->getParameter('access_token')); + $this->assertNotNull($response->getParameter('expires_in')); + $this->assertNotNull($response->getParameter('token_type')); + } + + public function testInvalidTokenTypeHintForRevoke() + { + $server = $this->getTestServer(); + + $request = TestRequest::createPost(array( + 'token_type_hint' => 'foo', + 'token' => 'sometoken' + )); + + $server->handleRevokeRequest($request, $response = new Response()); + + $this->assertTrue($response instanceof Response); + $this->assertEquals(400, $response->getStatusCode(), var_export($response, 1)); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Token type hint must be either \'access_token\' or \'refresh_token\''); + } + + public function testMissingTokenForRevoke() + { + $server = $this->getTestServer(); + + $request = TestRequest::createPost(array( + 'token_type_hint' => 'access_token' + )); + + $server->handleRevokeRequest($request, $response = new Response()); + $this->assertTrue($response instanceof Response); + $this->assertEquals(400, $response->getStatusCode(), var_export($response, 1)); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Missing token parameter to revoke'); + } + + public function testInvalidRequestMethodForRevoke() + { + $server = $this->getTestServer(); + + $request = new TestRequest(); + $request->setQuery(array( + 'token_type_hint' => 'access_token' + )); + + $server->handleRevokeRequest($request, $response = new Response()); + $this->assertTrue($response instanceof Response); + $this->assertEquals(405, $response->getStatusCode(), var_export($response, 1)); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'The request method must be POST when revoking an access token'); + } + + public function testCreateController() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $accessToken = new \OAuth2\ResponseType\AccessToken($storage); + $controller = new TokenController($accessToken, $storage); + } + + private function getTestServer() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage); + $server->addGrantType(new AuthorizationCode($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/FirebaseJwtTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/FirebaseJwtTest.php new file mode 100644 index 0000000..d341367 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/FirebaseJwtTest.php @@ -0,0 +1,102 @@ +privateKey = << $client_id, + 'exp' => time() + 1000, + 'iat' => time(), + 'sub' => 'testuser@ourdomain.com', + 'aud' => 'http://myapp.com/oauth/auth', + 'scope' => null, + ); + + $encoded = $jwtUtil->encode($params, $this->privateKey, 'RS256'); + + // test BC behaviour of trusting the algorithm in the header + $payload = $jwtUtil->decode($encoded, $client_key, array('RS256')); + $this->assertEquals($params, $payload); + + // test BC behaviour of not verifying by passing false + $payload = $jwtUtil->decode($encoded, $client_key, false); + $this->assertEquals($params, $payload); + + // test the new restricted algorithms header + $payload = $jwtUtil->decode($encoded, $client_key, array('RS256')); + $this->assertEquals($params, $payload); + } + + public function testInvalidJwt() + { + $jwtUtil = new FirebaseJwt(); + + $this->assertFalse($jwtUtil->decode('goob')); + $this->assertFalse($jwtUtil->decode('go.o.b')); + } + + /** @dataProvider provideClientCredentials */ + public function testInvalidJwtHeader($client_id, $client_key) + { + $jwtUtil = new FirebaseJwt(); + + $params = array( + 'iss' => $client_id, + 'exp' => time() + 1000, + 'iat' => time(), + 'sub' => 'testuser@ourdomain.com', + 'aud' => 'http://myapp.com/oauth/auth', + 'scope' => null, + ); + + // testing for algorithm tampering when only RSA256 signing is allowed + // @see https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/ + $tampered = $jwtUtil->encode($params, $client_key, 'HS256'); + + $payload = $jwtUtil->decode($tampered, $client_key, array('RS256')); + + $this->assertFalse($payload); + } + + public function provideClientCredentials() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $client_id = 'Test Client ID'; + $client_key = $storage->getClientKey($client_id, "testuser@ourdomain.com"); + + return array( + array($client_id, $client_key), + ); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/JwtTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/JwtTest.php new file mode 100644 index 0000000..214eeba --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/JwtTest.php @@ -0,0 +1,102 @@ +privateKey = << $client_id, + 'exp' => time() + 1000, + 'iat' => time(), + 'sub' => 'testuser@ourdomain.com', + 'aud' => 'http://myapp.com/oauth/auth', + 'scope' => null, + ); + + $encoded = $jwtUtil->encode($params, $this->privateKey, 'RS256'); + + // test BC behaviour of trusting the algorithm in the header + $payload = $jwtUtil->decode($encoded, $client_key); + $this->assertEquals($params, $payload); + + // test BC behaviour of not verifying by passing false + $payload = $jwtUtil->decode($encoded, $client_key, false); + $this->assertEquals($params, $payload); + + // test the new restricted algorithms header + $payload = $jwtUtil->decode($encoded, $client_key, array('RS256')); + $this->assertEquals($params, $payload); + } + + public function testInvalidJwt() + { + $jwtUtil = new Jwt(); + + $this->assertFalse($jwtUtil->decode('goob')); + $this->assertFalse($jwtUtil->decode('go.o.b')); + } + + /** @dataProvider provideClientCredentials */ + public function testInvalidJwtHeader($client_id, $client_key) + { + $jwtUtil = new Jwt(); + + $params = array( + 'iss' => $client_id, + 'exp' => time() + 1000, + 'iat' => time(), + 'sub' => 'testuser@ourdomain.com', + 'aud' => 'http://myapp.com/oauth/auth', + 'scope' => null, + ); + + // testing for algorithm tampering when only RSA256 signing is allowed + // @see https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/ + $tampered = $jwtUtil->encode($params, $client_key, 'HS256'); + + $payload = $jwtUtil->decode($tampered, $client_key, array('RS256')); + + $this->assertFalse($payload); + } + + public function provideClientCredentials() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $client_id = 'Test Client ID'; + $client_key = $storage->getClientKey($client_id, "testuser@ourdomain.com"); + + return array( + array($client_id, $client_key), + ); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/AuthorizationCodeTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/AuthorizationCodeTest.php new file mode 100644 index 0000000..7409896 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/AuthorizationCodeTest.php @@ -0,0 +1,207 @@ +getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Missing parameter: "code" is required'); + } + + public function testInvalidCode() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'InvalidCode', // invalid authorization code + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Authorization code doesn\'t exist or is invalid for the client'); + } + + public function testCodeCannotBeUsedTwice() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode', // valid code + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 200); + $this->assertNotNull($response->getParameter('access_token')); + + // try to use the same code again + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Authorization code doesn\'t exist or is invalid for the client'); + } + + public function testExpiredCode() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-expired', // expired authorization code + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'The authorization code has expired'); + } + + public function testValidCode() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode', // valid code + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + } + + public function testValidCodeNoScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-with-scope', // valid code + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('scope', $token); + $this->assertEquals($token['scope'], 'scope1 scope2'); + } + + public function testValidCodeSameScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-with-scope', // valid code + 'scope' => 'scope2 scope1', + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('scope', $token); + $this->assertEquals($token['scope'], 'scope2 scope1'); + } + + public function testValidCodeLessScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-with-scope', // valid code + 'scope' => 'scope1', + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('scope', $token); + $this->assertEquals($token['scope'], 'scope1'); + } + + public function testValidCodeDifferentScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-with-scope', // valid code + 'scope' => 'scope3', + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'The scope requested is invalid for this request'); + } + + public function testValidCodeInvalidScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-with-scope', // valid code + 'scope' => 'invalid-scope', + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'The scope requested is invalid for this request'); + } + + public function testValidClientDifferentCode() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Some Other Client', // valid client id + 'client_secret' => 'TestSecret3', // valid client secret + 'code' => 'testcode', // valid code + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'authorization_code doesn\'t exist or is invalid for the client'); + } + + private function getTestServer() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage); + $server->addGrantType(new AuthorizationCode($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ClientCredentialsTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ClientCredentialsTest.php new file mode 100644 index 0000000..f0d46cc --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ClientCredentialsTest.php @@ -0,0 +1,159 @@ +getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'FakeSecret', // valid client secret + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_client'); + $this->assertEquals($response->getParameter('error_description'), 'The client credentials are invalid'); + } + + public function testValidCredentials() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('scope', $token); + $this->assertNull($token['scope']); + } + + public function testValidCredentialsWithScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'scope' => 'scope1', + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('scope', $token); + $this->assertEquals($token['scope'], 'scope1'); + } + + public function testValidCredentialsInvalidScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'scope' => 'invalid-scope', + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'An unsupported scope was requested'); + } + + public function testValidCredentialsInHeader() + { + // create with HTTP_AUTHORIZATION in header + $server = $this->getTestServer(); + $headers = array('HTTP_AUTHORIZATION' => 'Basic '.base64_encode('Test Client ID:TestSecret'), 'REQUEST_METHOD' => 'POST'); + $params = array('grant_type' => 'client_credentials'); + $request = new Request(array(), $params, array(), array(), array(), $headers); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertNotNull($token['access_token']); + + // create using PHP Authorization Globals + $headers = array('PHP_AUTH_USER' => 'Test Client ID', 'PHP_AUTH_PW' => 'TestSecret', 'REQUEST_METHOD' => 'POST'); + $params = array('grant_type' => 'client_credentials'); + $request = new Request(array(), $params, array(), array(), array(), $headers); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertNotNull($token['access_token']); + } + + public function testValidCredentialsInRequest() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertNotNull($token['access_token']); + } + + public function testValidCredentialsInQuerystring() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertNotNull($token['access_token']); + } + + public function testClientUserIdIsSetInAccessToken() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Client ID With User ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + + // verify the user_id was associated with the token + $storage = $server->getStorage('client'); + $token = $storage->getAccessToken($token['access_token']); + $this->assertNotNull($token); + $this->assertArrayHasKey('user_id', $token); + $this->assertEquals($token['user_id'], 'brent@brentertainment.com'); + } + + private function getTestServer() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage); + $server->addGrantType(new ClientCredentials($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ImplicitTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ImplicitTest.php new file mode 100644 index 0000000..a312ebc --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ImplicitTest.php @@ -0,0 +1,143 @@ +getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'token', // invalid response type + )); + $server->handleAuthorizeRequest($request, $response = new Response(), false); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + parse_str($parts['query'], $query); + + $this->assertEquals($query['error'], 'unsupported_response_type'); + $this->assertEquals($query['error_description'], 'implicit grant type not supported'); + } + + public function testUserDeniesAccessResponse() + { + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'token', // valid response type + 'state' => 'xyz', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), false); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + parse_str($parts['query'], $query); + + $this->assertEquals($query['error'], 'access_denied'); + $this->assertEquals($query['error_description'], 'The user denied access to your application'); + } + + public function testSuccessfulRequestFragmentParameter() + { + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'token', // valid response type + 'state' => 'xyz', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $this->assertNull($response->getParameter('error')); + $this->assertNull($response->getParameter('error_description')); + + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + + $this->assertEquals('http', $parts['scheme']); // same as passed in to redirect_uri + $this->assertEquals('adobe.com', $parts['host']); // same as passed in to redirect_uri + $this->assertArrayHasKey('fragment', $parts); + $this->assertFalse(isset($parts['query'])); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['fragment'], $params); + $this->assertNotNull($params); + $this->assertArrayHasKey('access_token', $params); + $this->assertArrayHasKey('expires_in', $params); + $this->assertArrayHasKey('token_type', $params); + } + + public function testSuccessfulRequestReturnsStateParameter() + { + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'token', // valid response type + 'state' => 'test', // valid state string (just needs to be passed back to us) + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $this->assertNull($response->getParameter('error')); + $this->assertNull($response->getParameter('error_description')); + + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + $this->assertArrayHasKey('fragment', $parts); + parse_str($parts['fragment'], $params); + + $this->assertArrayHasKey('state', $params); + $this->assertEquals($params['state'], 'test'); + } + + public function testSuccessfulRequestStripsExtraParameters() + { + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com?fake=something', // valid redirect URI + 'response_type' => 'token', // valid response type + 'state' => 'test', // valid state string (just needs to be passed back to us) + 'fake' => 'something', // add extra param to querystring + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $this->assertNull($response->getParameter('error')); + $this->assertNull($response->getParameter('error_description')); + + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + $this->assertFalse(isset($parts['fake'])); + $this->assertArrayHasKey('fragment', $parts); + parse_str($parts['fragment'], $params); + + $this->assertFalse(isset($parmas['fake'])); + $this->assertArrayHasKey('state', $params); + $this->assertEquals($params['state'], 'test'); + } + + private function getTestServer($config = array()) + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage, $config); + + // Add the two types supported for authorization grant + $server->addGrantType(new AuthorizationCode($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/JwtBearerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/JwtBearerTest.php new file mode 100644 index 0000000..0a6c4b8 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/JwtBearerTest.php @@ -0,0 +1,360 @@ +privateKey = <<getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + )); + + //Get the jwt and break it + $jwt = $this->getJWT(); + $jwt = substr_replace($jwt, 'broken', 3, 6); + + $request->request['assertion'] = $jwt; + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'JWT is malformed'); + } + + public function testBrokenSignature() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + )); + + //Get the jwt and break signature + $jwt = $this->getJWT() . 'notSupposeToBeHere'; + $request->request['assertion'] = $jwt; + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'JWT failed signature verification'); + } + + public function testExpiredJWT() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + )); + + //Get an expired JWT + $jwt = $this->getJWT(1234); + $request->request['assertion'] = $jwt; + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'JWT has expired'); + } + + public function testBadExp() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + )); + + //Get an expired JWT + $jwt = $this->getJWT('badtimestamp'); + $request->request['assertion'] = $jwt; + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Expiration (exp) time must be a unix time stamp'); + } + + public function testNoAssert() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + )); + + //Do not pass the assert (JWT) + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Missing parameters: "assertion" required'); + } + + public function testNotBefore() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + )); + + //Get a future NBF + $jwt = $this->getJWT(null, time() + 10000); + $request->request['assertion'] = $jwt; + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'JWT cannot be used before the Not Before (nbf) time'); + } + + public function testBadNotBefore() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + )); + + //Get a non timestamp nbf + $jwt = $this->getJWT(null, 'notatimestamp'); + $request->request['assertion'] = $jwt; + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Not Before (nbf) time must be a unix time stamp'); + } + + public function testNonMatchingAudience() + { + $server = $this->getTestServer('http://google.com/oauth/o/auth'); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(), + )); + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Invalid audience (aud)'); + } + + public function testBadClientID() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(null, null, null, 'bad_client_id'), + )); + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Invalid issuer (iss) or subject (sub) provided'); + } + + public function testBadSubject() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(null, null, 'anotheruser@ourdomain,com'), + )); + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Invalid issuer (iss) or subject (sub) provided'); + } + + public function testMissingKey() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(null, null, null, 'Missing Key Cli,nt'), + )); + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Invalid issuer (iss) or subject (sub) provided'); + } + + public function testValidJwt() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(), // valid assertion + )); + + $token = $server->grantAccessToken($request, new Response()); + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + } + + public function testValidJwtWithScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(null, null, null, 'Test Client ID'), // valid assertion + 'scope' => 'scope1', // valid scope + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('scope', $token); + $this->assertEquals($token['scope'], 'scope1'); + } + + public function testValidJwtInvalidScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(null, null, null, 'Test Client ID'), // valid assertion + 'scope' => 'invalid-scope', // invalid scope + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'An unsupported scope was requested'); + } + + public function testValidJti() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(null, null, 'testuser@ourdomain.com', 'Test Client ID', 'unused_jti'), // valid assertion with invalid scope + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + } + + public function testInvalidJti() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(99999999900, null, 'testuser@ourdomain.com', 'Test Client ID', 'used_jti'), // valid assertion with invalid scope + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'JSON Token Identifier (jti) has already been used'); + } + + public function testJtiReplayAttack() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(99999999900, null, 'testuser@ourdomain.com', 'Test Client ID', 'totally_new_jti'), // valid assertion with invalid scope + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + + //Replay the same request + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'JSON Token Identifier (jti) has already been used'); + } + + /** + * Generates a JWT + * @param $exp The expiration date. If the current time is greater than the exp, the JWT is invalid. + * @param $nbf The "not before" time. If the current time is less than the nbf, the JWT is invalid. + * @param $sub The subject we are acting on behalf of. This could be the email address of the user in the system. + * @param $iss The issuer, usually the client_id. + * @return string + */ + private function getJWT($exp = null, $nbf = null, $sub = null, $iss = 'Test Client ID', $jti = null) + { + if (!$exp) { + $exp = time() + 1000; + } + + if (!$sub) { + $sub = "testuser@ourdomain.com"; + } + + $params = array( + 'iss' => $iss, + 'exp' => $exp, + 'iat' => time(), + 'sub' => $sub, + 'aud' => 'http://myapp.com/oauth/auth', + ); + + if ($nbf) { + $params['nbf'] = $nbf; + } + + if ($jti) { + $params['jti'] = $jti; + } + + $jwtUtil = new Jwt(); + + return $jwtUtil->encode($params, $this->privateKey, 'RS256'); + } + + private function getTestServer($audience = 'http://myapp.com/oauth/auth') + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage); + $server->addGrantType(new JwtBearer($storage, $audience, new Jwt())); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/RefreshTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/RefreshTokenTest.php new file mode 100644 index 0000000..a458aad --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/RefreshTokenTest.php @@ -0,0 +1,204 @@ +getTestServer(); + $server->addGrantType(new RefreshToken($this->storage)); + + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Missing parameter: "refresh_token" is required'); + } + + public function testInvalidRefreshToken() + { + $server = $this->getTestServer(); + $server->addGrantType(new RefreshToken($this->storage)); + + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'fake-token', // invalid refresh token + )); + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Invalid refresh token'); + } + + public function testValidRefreshTokenWithNewRefreshTokenInResponse() + { + $server = $this->getTestServer(); + $server->addGrantType(new RefreshToken($this->storage, array('always_issue_new_refresh_token' => true))); + + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'test-refreshtoken', // valid refresh token + )); + $token = $server->grantAccessToken($request, new Response()); + $this->assertTrue(isset($token['refresh_token']), 'refresh token should always refresh'); + + $refresh_token = $this->storage->getRefreshToken($token['refresh_token']); + $this->assertNotNull($refresh_token); + $this->assertEquals($refresh_token['refresh_token'], $token['refresh_token']); + $this->assertEquals($refresh_token['client_id'], $request->request('client_id')); + $this->assertTrue($token['refresh_token'] != 'test-refreshtoken', 'the refresh token returned is not the one used'); + $used_token = $this->storage->getRefreshToken('test-refreshtoken'); + $this->assertFalse($used_token, 'the refresh token used is no longer valid'); + } + + public function testValidRefreshTokenDoesNotUnsetToken() + { + $server = $this->getTestServer(); + $server->addGrantType(new RefreshToken($this->storage, array( + 'always_issue_new_refresh_token' => true, + 'unset_refresh_token_after_use' => false, + ))); + + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'test-refreshtoken', // valid refresh token + )); + $token = $server->grantAccessToken($request, new Response()); + $this->assertTrue(isset($token['refresh_token']), 'refresh token should always refresh'); + + $used_token = $this->storage->getRefreshToken('test-refreshtoken'); + $this->assertNotNull($used_token, 'the refresh token used is still valid'); + } + + public function testValidRefreshTokenWithNoRefreshTokenInResponse() + { + $server = $this->getTestServer(); + $server->addGrantType(new RefreshToken($this->storage, array('always_issue_new_refresh_token' => false))); + + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'test-refreshtoken', // valid refresh token + )); + $token = $server->grantAccessToken($request, new Response()); + $this->assertFalse(isset($token['refresh_token']), 'refresh token should not be returned'); + + $used_token = $this->storage->getRefreshToken('test-refreshtoken'); + $this->assertNotNull($used_token, 'the refresh token used is still valid'); + } + + public function testValidRefreshTokenSameScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'test-refreshtoken-with-scope', // valid refresh token (with scope) + 'scope' => 'scope2 scope1', + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('scope', $token); + $this->assertEquals($token['scope'], 'scope2 scope1'); + } + + public function testValidRefreshTokenLessScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'test-refreshtoken-with-scope', // valid refresh token (with scope) + 'scope' => 'scope1', + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('scope', $token); + $this->assertEquals($token['scope'], 'scope1'); + } + + public function testValidRefreshTokenDifferentScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'test-refreshtoken-with-scope', // valid refresh token (with scope) + 'scope' => 'scope3', + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'The scope requested is invalid for this request'); + } + + public function testValidRefreshTokenInvalidScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'test-refreshtoken-with-scope', // valid refresh token (with scope) + 'scope' => 'invalid-scope', + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'The scope requested is invalid for this request'); + } + + public function testValidClientDifferentRefreshToken() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Some Other Client', // valid client id + 'client_secret' => 'TestSecret3', // valid client secret + 'refresh_token' => 'test-refreshtoken', // valid refresh token + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'refresh_token doesn\'t exist or is invalid for the client'); + } + + private function getTestServer() + { + $this->storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($this->storage); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/UserCredentialsTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/UserCredentialsTest.php new file mode 100644 index 0000000..18943d0 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/UserCredentialsTest.php @@ -0,0 +1,172 @@ +getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'password' => 'testpass', // valid password + )); + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Missing parameters: "username" and "password" required'); + } + + public function testNoPassword() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'username' => 'test-username', // valid username + )); + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Missing parameters: "username" and "password" required'); + } + + public function testInvalidUsername() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'username' => 'fake-username', // valid username + 'password' => 'testpass', // valid password + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 401); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Invalid username and password combination'); + } + + public function testInvalidPassword() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'username' => 'test-username', // valid username + 'password' => 'fakepass', // invalid password + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 401); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Invalid username and password combination'); + } + + public function testValidCredentials() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'username' => 'test-username', // valid username + 'password' => 'testpass', // valid password + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + } + + public function testValidCredentialsWithScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'username' => 'test-username', // valid username + 'password' => 'testpass', // valid password + 'scope' => 'scope1', // valid scope + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('scope', $token); + $this->assertEquals($token['scope'], 'scope1'); + } + + public function testValidCredentialsInvalidScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'username' => 'test-username', // valid username + 'password' => 'testpass', // valid password + 'scope' => 'invalid-scope', + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'An unsupported scope was requested'); + } + + public function testNoSecretWithPublicClient() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID Empty Secret', // valid public client + 'username' => 'test-username', // valid username + 'password' => 'testpass', // valid password + )); + + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + } + + public function testNoSecretWithConfidentialClient() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid public client + 'username' => 'test-username', // valid username + 'password' => 'testpass', // valid password + )); + + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_client'); + $this->assertEquals($response->getParameter('error_description'), 'This client is invalid or must authenticate using a client secret'); + } + + private function getTestServer() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage); + $server->addGrantType(new UserCredentials($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php new file mode 100644 index 0000000..46de936 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php @@ -0,0 +1,182 @@ +getTestServer(); + + $response = new Response(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'id_token', + 'state' => 'af0ifjsldkj', + 'nonce' => 'n-0S6_WzA2Mj', + )); + + // Test valid id_token request + $server->handleAuthorizeRequest($request, $response, true); + + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['fragment'], $query); + + $this->assertEquals('n-0S6_WzA2Mj', $server->getAuthorizeController()->getNonce()); + $this->assertEquals($query['state'], 'af0ifjsldkj'); + + $this->assertArrayHasKey('id_token', $query); + $this->assertArrayHasKey('state', $query); + $this->assertArrayNotHasKey('access_token', $query); + $this->assertArrayNotHasKey('expires_in', $query); + $this->assertArrayNotHasKey('token_type', $query); + + // Test valid token id_token request + $request->query['response_type'] = 'id_token token'; + $server->handleAuthorizeRequest($request, $response, true); + + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['fragment'], $query); + + $this->assertEquals('n-0S6_WzA2Mj', $server->getAuthorizeController()->getNonce()); + $this->assertEquals($query['state'], 'af0ifjsldkj'); + + $this->assertArrayHasKey('access_token', $query); + $this->assertArrayHasKey('expires_in', $query); + $this->assertArrayHasKey('token_type', $query); + $this->assertArrayHasKey('state', $query); + $this->assertArrayHasKey('id_token', $query); + + // assert that with multiple-valued response types, order does not matter + $request->query['response_type'] = 'token id_token'; + $server->handleAuthorizeRequest($request, $response, true); + + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['fragment'], $query); + + $this->assertEquals('n-0S6_WzA2Mj', $server->getAuthorizeController()->getNonce()); + $this->assertEquals($query['state'], 'af0ifjsldkj'); + + $this->assertArrayHasKey('access_token', $query); + $this->assertArrayHasKey('expires_in', $query); + $this->assertArrayHasKey('token_type', $query); + $this->assertArrayHasKey('state', $query); + $this->assertArrayHasKey('id_token', $query); + + // assert that with multiple-valued response types with extra spaces do not matter + $request->query['response_type'] = ' token id_token '; + $server->handleAuthorizeRequest($request, $response, true); + + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['fragment'], $query); + + $this->assertEquals('n-0S6_WzA2Mj', $server->getAuthorizeController()->getNonce()); + $this->assertEquals($query['state'], 'af0ifjsldkj'); + + $this->assertArrayHasKey('access_token', $query); + $this->assertArrayHasKey('expires_in', $query); + $this->assertArrayHasKey('token_type', $query); + $this->assertArrayHasKey('state', $query); + $this->assertArrayHasKey('id_token', $query); + } + + public function testMissingNonce() + { + $server = $this->getTestServer(); + $authorize = $server->getAuthorizeController(); + + $response = new Response(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'id_token', + 'state' => 'xyz', + )); + + // Test missing nonce for 'id_token' response type + $server->handleAuthorizeRequest($request, $response, true); + $params = $response->getParameters(); + + $this->assertEquals($params['error'], 'invalid_nonce'); + $this->assertEquals($params['error_description'], 'This application requires you specify a nonce parameter'); + + // Test missing nonce for 'id_token token' response type + $request->query['response_type'] = 'id_token token'; + $server->handleAuthorizeRequest($request, $response, true); + $params = $response->getParameters(); + + $this->assertEquals($params['error'], 'invalid_nonce'); + $this->assertEquals($params['error_description'], 'This application requires you specify a nonce parameter'); + } + + public function testNotGrantedApplication() + { + $server = $this->getTestServer(); + + $response = new Response(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'id_token', + 'state' => 'af0ifjsldkj', + 'nonce' => 'n-0S6_WzA2Mj', + )); + + // Test not approved application + $server->handleAuthorizeRequest($request, $response, false); + + $params = $response->getParameters(); + + $this->assertEquals($params['error'], 'consent_required'); + $this->assertEquals($params['error_description'], 'The user denied access to your application'); + + // Test not approved application with prompt parameter + $request->query['prompt'] = 'none'; + $server->handleAuthorizeRequest($request, $response, false); + + $params = $response->getParameters(); + + $this->assertEquals($params['error'], 'login_required'); + $this->assertEquals($params['error_description'], 'The user must log in'); + + // Test not approved application with user_id set + $request->query['prompt'] = 'none'; + $server->handleAuthorizeRequest($request, $response, false, 'some-user-id'); + + $params = $response->getParameters(); + + $this->assertEquals($params['error'], 'interaction_required'); + $this->assertEquals($params['error_description'], 'The user must grant access to your application'); + } + + public function testNeedsIdToken() + { + $server = $this->getTestServer(); + $authorize = $server->getAuthorizeController(); + + $this->assertTrue($authorize->needsIdToken('openid')); + $this->assertTrue($authorize->needsIdToken('openid profile')); + $this->assertFalse($authorize->needsIdToken('')); + $this->assertFalse($authorize->needsIdToken('some-scope')); + } + + private function getTestServer($config = array()) + { + $config += array( + 'use_openid_connect' => true, + 'issuer' => 'phpunit', + 'allow_implicit' => true + ); + + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage, $config); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php new file mode 100644 index 0000000..b1b6870 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php @@ -0,0 +1,44 @@ +handleUserInfoRequest(new Request(), $response); + $this->assertEquals(401, $response->getStatusCode()); + } + + public function testValidToken() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-openid-connect'; + $response = new Response(); + + $server->handleUserInfoRequest($request, $response); + $parameters = $response->getParameters(); + $this->assertEquals($parameters['sub'], 'testuser'); + $this->assertEquals($parameters['email'], 'testuser@test.com'); + $this->assertEquals($parameters['email_verified'], true); + } + + private function getTestServer($config = array()) + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage, $config); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php new file mode 100644 index 0000000..776002d --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php @@ -0,0 +1,57 @@ +getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-openid', // valid code + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('id_token', $token); + $this->assertEquals('test_id_token', $token['id_token']); + + // this is only true if "offline_access" was requested + $this->assertFalse(isset($token['refresh_token'])); + } + + public function testOfflineAccess() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-openid', // valid code + 'scope' => 'offline_access', // valid code + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('id_token', $token); + $this->assertEquals('test_id_token', $token['id_token']); + $this->assertTrue(isset($token['refresh_token'])); + } + + private function getTestServer() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage, array('use_openid_connect' => true)); + $server->addGrantType(new AuthorizationCode($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php new file mode 100644 index 0000000..5daaaa6 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php @@ -0,0 +1,91 @@ +getTestServer(); + + $request = new Request(array( + 'response_type' => 'code id_token', + 'redirect_uri' => 'http://adobe.com', + 'client_id' => 'Test Client ID', + 'scope' => 'openid', + 'state' => 'test', + 'nonce' => 'test', + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $this->assertNotContains('error', $location); + + $parts = parse_url($location); + $this->assertArrayHasKey('query', $parts); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['query'], $params); + $this->assertNotNull($params); + $this->assertArrayHasKey('id_token', $params); + $this->assertArrayHasKey('code', $params); + + // validate ID Token + $parts = explode('.', $params['id_token']); + foreach ($parts as &$part) { + // Each part is a base64url encoded json string. + $part = str_replace(array('-', '_'), array('+', '/'), $part); + $part = base64_decode($part); + $part = json_decode($part, true); + } + list($header, $claims, $signature) = $parts; + + $this->assertArrayHasKey('iss', $claims); + $this->assertArrayHasKey('sub', $claims); + $this->assertArrayHasKey('aud', $claims); + $this->assertArrayHasKey('iat', $claims); + $this->assertArrayHasKey('exp', $claims); + $this->assertArrayHasKey('auth_time', $claims); + $this->assertArrayHasKey('nonce', $claims); + + // only exists if an access token was granted along with the id_token + $this->assertArrayNotHasKey('at_hash', $claims); + + $this->assertEquals($claims['iss'], 'test'); + $this->assertEquals($claims['aud'], 'Test Client ID'); + $this->assertEquals($claims['nonce'], 'test'); + $duration = $claims['exp'] - $claims['iat']; + $this->assertEquals($duration, 3600); + } + + private function getTestServer($config = array()) + { + $config += array( + 'use_openid_connect' => true, + 'issuer' => 'test', + 'id_lifetime' => 3600, + 'allow_implicit' => true, + ); + + $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); + $responseTypes = array( + 'code' => $code = new AuthorizationCode($memoryStorage), + 'id_token' => $idToken = new IdToken($memoryStorage, $memoryStorage, $config), + 'code id_token' => new CodeIdToken($code, $idToken), + ); + + $server = new Server($memoryStorage, $config, array(), $responseTypes); + $server->addGrantType(new ClientCredentials($memoryStorage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTest.php new file mode 100644 index 0000000..e772f6b --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTest.php @@ -0,0 +1,184 @@ + 'id_token', + 'redirect_uri' => 'http://adobe.com', + 'client_id' => 'Test Client ID', + 'scope' => 'openid', + 'state' => 'test', + ); + + // attempt to do the request without a nonce. + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request($query); + $valid = $server->validateAuthorizeRequest($request, $response = new Response()); + + // Add a nonce and retry. + $query['nonce'] = 'test'; + $request = new Request($query); + $valid = $server->validateAuthorizeRequest($request, $response = new Response()); + $this->assertTrue($valid); + } + + public function testHandleAuthorizeRequest() + { + // add the test parameters in memory + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( + 'response_type' => 'id_token', + 'redirect_uri' => 'http://adobe.com', + 'client_id' => 'Test Client ID', + 'scope' => 'openid email', + 'state' => 'test', + 'nonce' => 'test', + )); + + $user_id = 'testuser'; + $server->handleAuthorizeRequest($request, $response = new Response(), true, $user_id); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $this->assertNotContains('error', $location); + + $parts = parse_url($location); + $this->assertArrayHasKey('fragment', $parts); + $this->assertFalse(isset($parts['query'])); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['fragment'], $params); + $this->assertNotNull($params); + $this->assertArrayHasKey('id_token', $params); + $this->assertArrayNotHasKey('access_token', $params); + $this->validateIdToken($params['id_token']); + } + + public function testPassInAuthTime() + { + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( + 'response_type' => 'id_token', + 'redirect_uri' => 'http://adobe.com', + 'client_id' => 'Test Client ID', + 'scope' => 'openid email', + 'state' => 'test', + 'nonce' => 'test', + )); + + // test with a scalar user id + $user_id = 'testuser123'; + $server->handleAuthorizeRequest($request, $response = new Response(), true, $user_id); + + list($header, $payload, $signature) = $this->extractTokenDataFromResponse($response); + + $this->assertTrue(is_array($payload)); + $this->assertArrayHasKey('sub', $payload); + $this->assertEquals($user_id, $payload['sub']); + $this->assertArrayHasKey('auth_time', $payload); + + // test with an array of user info + $userInfo = array( + 'user_id' => 'testuser1234', + 'auth_time' => date('Y-m-d H:i:s', strtotime('20 minutes ago') + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true, $userInfo); + + list($header, $payload, $signature) = $this->extractTokenDataFromResponse($response); + + $this->assertTrue(is_array($payload)); + $this->assertArrayHasKey('sub', $payload); + $this->assertEquals($userInfo['user_id'], $payload['sub']); + $this->assertArrayHasKey('auth_time', $payload); + $this->assertEquals($userInfo['auth_time'], $payload['auth_time']); + } + + private function extractTokenDataFromResponse(Response $response) + { + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $this->assertNotContains('error', $location); + + $parts = parse_url($location); + $this->assertArrayHasKey('fragment', $parts); + $this->assertFalse(isset($parts['query'])); + + parse_str($parts['fragment'], $params); + $this->assertNotNull($params); + $this->assertArrayHasKey('id_token', $params); + $this->assertArrayNotHasKey('access_token', $params); + + list($headb64, $payloadb64, $signature) = explode('.', $params['id_token']); + + $jwt = new Jwt(); + $header = json_decode($jwt->urlSafeB64Decode($headb64), true); + $payload = json_decode($jwt->urlSafeB64Decode($payloadb64), true); + + return array($header, $payload, $signature); + } + + private function validateIdToken($id_token) + { + $parts = explode('.', $id_token); + foreach ($parts as &$part) { + // Each part is a base64url encoded json string. + $part = str_replace(array('-', '_'), array('+', '/'), $part); + $part = base64_decode($part); + $part = json_decode($part, true); + } + list($header, $claims, $signature) = $parts; + + $this->assertArrayHasKey('iss', $claims); + $this->assertArrayHasKey('sub', $claims); + $this->assertArrayHasKey('aud', $claims); + $this->assertArrayHasKey('iat', $claims); + $this->assertArrayHasKey('exp', $claims); + $this->assertArrayHasKey('auth_time', $claims); + $this->assertArrayHasKey('nonce', $claims); + $this->assertArrayHasKey('email', $claims); + $this->assertArrayHasKey('email_verified', $claims); + + $this->assertEquals($claims['iss'], 'test'); + $this->assertEquals($claims['aud'], 'Test Client ID'); + $this->assertEquals($claims['nonce'], 'test'); + $this->assertEquals($claims['email'], 'testuser@test.com'); + $duration = $claims['exp'] - $claims['iat']; + $this->assertEquals($duration, 3600); + } + + private function getTestServer($config = array()) + { + $config += array( + 'use_openid_connect' => true, + 'issuer' => 'test', + 'id_lifetime' => 3600, + ); + + $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); + $memoryStorage->supportedScopes[] = 'email'; + $storage = array( + 'client' => $memoryStorage, + 'scope' => $memoryStorage, + ); + $responseTypes = array( + 'id_token' => new IdToken($memoryStorage, $memoryStorage, $config), + ); + + $server = new Server($storage, $config, array(), $responseTypes); + $server->addGrantType(new ClientCredentials($memoryStorage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php new file mode 100644 index 0000000..bc564d3 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php @@ -0,0 +1,91 @@ +getTestServer(array('allow_implicit' => true)); + + $request = new Request(array( + 'response_type' => 'id_token token', + 'redirect_uri' => 'http://adobe.com', + 'client_id' => 'Test Client ID', + 'scope' => 'openid', + 'state' => 'test', + 'nonce' => 'test', + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $this->assertNotContains('error', $location); + + $parts = parse_url($location); + $this->assertArrayHasKey('fragment', $parts); + $this->assertFalse(isset($parts['query'])); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['fragment'], $params); + $this->assertNotNull($params); + $this->assertArrayHasKey('id_token', $params); + $this->assertArrayHasKey('access_token', $params); + + // validate ID Token + $parts = explode('.', $params['id_token']); + foreach ($parts as &$part) { + // Each part is a base64url encoded json string. + $part = str_replace(array('-', '_'), array('+', '/'), $part); + $part = base64_decode($part); + $part = json_decode($part, true); + } + list($header, $claims, $signature) = $parts; + + $this->assertArrayHasKey('iss', $claims); + $this->assertArrayHasKey('sub', $claims); + $this->assertArrayHasKey('aud', $claims); + $this->assertArrayHasKey('iat', $claims); + $this->assertArrayHasKey('exp', $claims); + $this->assertArrayHasKey('auth_time', $claims); + $this->assertArrayHasKey('nonce', $claims); + $this->assertArrayHasKey('at_hash', $claims); + + $this->assertEquals($claims['iss'], 'test'); + $this->assertEquals($claims['aud'], 'Test Client ID'); + $this->assertEquals($claims['nonce'], 'test'); + $duration = $claims['exp'] - $claims['iat']; + $this->assertEquals($duration, 3600); + } + + private function getTestServer($config = array()) + { + $config += array( + 'use_openid_connect' => true, + 'issuer' => 'test', + 'id_lifetime' => 3600, + ); + + $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); + $responseTypes = array( + 'token' => $token = new AccessToken($memoryStorage, $memoryStorage), + 'id_token' => $idToken = new IdToken($memoryStorage, $memoryStorage, $config), + 'id_token token' => new IdTokenToken($token, $idToken), + ); + + $server = new Server($memoryStorage, $config, array(), $responseTypes); + $server->addGrantType(new ClientCredentials($memoryStorage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php new file mode 100644 index 0000000..bdfb085 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php @@ -0,0 +1,95 @@ +markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + if (!$storage instanceof AuthorizationCodeInterface) { + return; + } + + // assert code we are about to add does not exist + $code = $storage->getAuthorizationCode('new-openid-code'); + $this->assertFalse($code); + + // add new code + $expires = time() + 20; + $scope = null; + $id_token = 'fake_id_token'; + $success = $storage->setAuthorizationCode('new-openid-code', 'client ID', 'SOMEUSERID', 'http://example.com', $expires, $scope, $id_token); + $this->assertTrue($success); + + $code = $storage->getAuthorizationCode('new-openid-code'); + $this->assertNotNull($code); + $this->assertArrayHasKey('authorization_code', $code); + $this->assertArrayHasKey('client_id', $code); + $this->assertArrayHasKey('user_id', $code); + $this->assertArrayHasKey('redirect_uri', $code); + $this->assertArrayHasKey('expires', $code); + $this->assertEquals($code['authorization_code'], 'new-openid-code'); + $this->assertEquals($code['client_id'], 'client ID'); + $this->assertEquals($code['user_id'], 'SOMEUSERID'); + $this->assertEquals($code['redirect_uri'], 'http://example.com'); + $this->assertEquals($code['expires'], $expires); + $this->assertEquals($code['id_token'], $id_token); + + // change existing code + $expires = time() + 42; + $new_id_token = 'fake_id_token-2'; + $success = $storage->setAuthorizationCode('new-openid-code', 'client ID2', 'SOMEOTHERID', 'http://example.org', $expires, $scope, $new_id_token); + $this->assertTrue($success); + + $code = $storage->getAuthorizationCode('new-openid-code'); + $this->assertNotNull($code); + $this->assertArrayHasKey('authorization_code', $code); + $this->assertArrayHasKey('client_id', $code); + $this->assertArrayHasKey('user_id', $code); + $this->assertArrayHasKey('redirect_uri', $code); + $this->assertArrayHasKey('expires', $code); + $this->assertEquals($code['authorization_code'], 'new-openid-code'); + $this->assertEquals($code['client_id'], 'client ID2'); + $this->assertEquals($code['user_id'], 'SOMEOTHERID'); + $this->assertEquals($code['redirect_uri'], 'http://example.org'); + $this->assertEquals($code['expires'], $expires); + $this->assertEquals($code['id_token'], $new_id_token); + } + + /** @dataProvider provideStorage */ + public function testRemoveIdTokenFromAuthorizationCode($storage) + { + // add new code + $expires = time() + 20; + $scope = null; + $id_token = 'fake_id_token_to_remove'; + $authcode = 'new-openid-code-'.rand(); + $success = $storage->setAuthorizationCode($authcode, 'client ID', 'SOMEUSERID', 'http://example.com', $expires, $scope, $id_token); + $this->assertTrue($success); + + // verify params were set + $code = $storage->getAuthorizationCode($authcode); + $this->assertNotNull($code); + $this->assertArrayHasKey('id_token', $code); + $this->assertEquals($code['id_token'], $id_token); + + // remove the id_token + $success = $storage->setAuthorizationCode($authcode, 'client ID', 'SOMEUSERID', 'http://example.com', $expires, $scope, null); + + // verify the "id_token" is now null + $code = $storage->getAuthorizationCode($authcode); + $this->assertNotNull($code); + $this->assertArrayHasKey('id_token', $code); + $this->assertEquals($code['id_token'], null); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/UserClaimsTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/UserClaimsTest.php new file mode 100644 index 0000000..840f6c5 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/UserClaimsTest.php @@ -0,0 +1,41 @@ +markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + if (!$storage instanceof UserClaimsInterface) { + // incompatible storage + return; + } + + // invalid user + $claims = $storage->getUserClaims('fake-user', ''); + $this->assertFalse($claims); + + // valid user (no scope) + $claims = $storage->getUserClaims('testuser', ''); + + /* assert the decoded token is the same */ + $this->assertFalse(isset($claims['email'])); + + // valid user + $claims = $storage->getUserClaims('testuser', 'email'); + + /* assert the decoded token is the same */ + $this->assertEquals($claims['email'], "testuser@test.com"); + $this->assertEquals($claims['email_verified'], true); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/RequestTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/RequestTest.php new file mode 100644 index 0000000..10db321 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/RequestTest.php @@ -0,0 +1,98 @@ +getTestServer(); + + // Smoke test for override request class + // $server->handleTokenRequest($request, $response = new Response()); + // $this->assertInstanceOf('Response', $response); + // $server->handleAuthorizeRequest($request, $response = new Response(), true); + // $this->assertInstanceOf('Response', $response); + // $response = $server->verifyResourceRequest($request, $response = new Response()); + // $this->assertTrue(is_bool($response)); + + /*** make some valid requests ***/ + + // Valid Token Request + $request->setPost(array( + 'grant_type' => 'authorization_code', + 'client_id' => 'Test Client ID', + 'client_secret' => 'TestSecret', + 'code' => 'testcode', + )); + $server->handleTokenRequest($request, $response = new Response()); + $this->assertEquals($response->getStatusCode(), 200); + $this->assertNull($response->getParameter('error')); + $this->assertNotNUll($response->getParameter('access_token')); + } + + public function testHeadersReturnsValueByKey() + { + $request = new Request( + array(), + array(), + array(), + array(), + array(), + array(), + array(), + array('AUTHORIZATION' => 'Basic secret') + ); + + $this->assertEquals('Basic secret', $request->headers('AUTHORIZATION')); + } + + public function testHeadersReturnsDefaultIfHeaderNotPresent() + { + $request = new Request(); + + $this->assertEquals('Bearer', $request->headers('AUTHORIZATION', 'Bearer')); + } + + public function testHeadersIsCaseInsensitive() + { + $request = new Request( + array(), + array(), + array(), + array(), + array(), + array(), + array(), + array('AUTHORIZATION' => 'Basic secret') + ); + + $this->assertEquals('Basic secret', $request->headers('Authorization')); + } + + public function testRequestReturnsPostParamIfNoQueryParamAvailable() + { + $request = new Request( + array(), + array('client_id' => 'correct') + ); + + $this->assertEquals('correct', $request->query('client_id', $request->request('client_id'))); + } + + private function getTestServer($config = array()) + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage, $config); + + // Add the two types supported for authorization grant + $server->addGrantType(new AuthorizationCode($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseTest.php new file mode 100644 index 0000000..b814900 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseTest.php @@ -0,0 +1,17 @@ + 'bar', + 'halland' => 'oates', + )); + + $string = $response->getResponseBody('xml'); + $this->assertContains('baroates', $string); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseType/AccessTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseType/AccessTokenTest.php new file mode 100644 index 0000000..0ed1c82 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseType/AccessTokenTest.php @@ -0,0 +1,107 @@ + array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke')); + $accessToken = new AccessToken($tokenStorage); + $accessToken->revokeToken('revoke', 'access_token'); + $this->assertFalse($tokenStorage->getAccessToken('revoke')); + } + + public function testRevokeAccessTokenWithoutTypeHint() + { + $tokenStorage = new Memory(array( + 'access_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke')); + $accessToken = new AccessToken($tokenStorage); + $accessToken->revokeToken('revoke'); + $this->assertFalse($tokenStorage->getAccessToken('revoke')); + } + + public function testRevokeRefreshTokenWithTypeHint() + { + $tokenStorage = new Memory(array( + 'refresh_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getRefreshToken('revoke')); + $accessToken = new AccessToken(new Memory, $tokenStorage); + $accessToken->revokeToken('revoke', 'refresh_token'); + $this->assertFalse($tokenStorage->getRefreshToken('revoke')); + } + + public function testRevokeRefreshTokenWithoutTypeHint() + { + $tokenStorage = new Memory(array( + 'refresh_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getRefreshToken('revoke')); + $accessToken = new AccessToken(new Memory, $tokenStorage); + $accessToken->revokeToken('revoke'); + $this->assertFalse($tokenStorage->getRefreshToken('revoke')); + } + + public function testRevokeAccessTokenWithRefreshTokenTypeHint() + { + $tokenStorage = new Memory(array( + 'access_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke')); + $accessToken = new AccessToken($tokenStorage); + $accessToken->revokeToken('revoke', 'refresh_token'); + $this->assertFalse($tokenStorage->getAccessToken('revoke')); + } + + public function testRevokeAccessTokenWithBogusTypeHint() + { + $tokenStorage = new Memory(array( + 'access_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke')); + $accessToken = new AccessToken($tokenStorage); + $accessToken->revokeToken('revoke', 'foo'); + $this->assertFalse($tokenStorage->getAccessToken('revoke')); + } + + public function testRevokeRefreshTokenWithBogusTypeHint() + { + $tokenStorage = new Memory(array( + 'refresh_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getRefreshToken('revoke')); + $accessToken = new AccessToken(new Memory, $tokenStorage); + $accessToken->revokeToken('revoke', 'foo'); + $this->assertFalse($tokenStorage->getRefreshToken('revoke')); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseType/JwtAccessTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseType/JwtAccessTokenTest.php new file mode 100644 index 0000000..51b01a9 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseType/JwtAccessTokenTest.php @@ -0,0 +1,160 @@ +getTestServer(); + $jwtResponseType = $server->getResponseType('token'); + + $accessToken = $jwtResponseType->createAccessToken('Test Client ID', 123, 'test', false); + $jwt = new Jwt; + $decodedAccessToken = $jwt->decode($accessToken['access_token'], null, false); + + $this->assertArrayHasKey('id', $decodedAccessToken); + $this->assertArrayHasKey('jti', $decodedAccessToken); + $this->assertArrayHasKey('iss', $decodedAccessToken); + $this->assertArrayHasKey('aud', $decodedAccessToken); + $this->assertArrayHasKey('exp', $decodedAccessToken); + $this->assertArrayHasKey('iat', $decodedAccessToken); + $this->assertArrayHasKey('token_type', $decodedAccessToken); + $this->assertArrayHasKey('scope', $decodedAccessToken); + + $this->assertEquals('https://api.example.com', $decodedAccessToken['iss']); + $this->assertEquals('Test Client ID', $decodedAccessToken['aud']); + $this->assertEquals(123, $decodedAccessToken['sub']); + $delta = $decodedAccessToken['exp'] - $decodedAccessToken['iat']; + $this->assertEquals(3600, $delta); + $this->assertEquals($decodedAccessToken['id'], $decodedAccessToken['jti']); + } + + public function testGrantJwtAccessToken() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertNotNull($response->getParameter('access_token')); + $this->assertEquals(2, substr_count($response->getParameter('access_token'), '.')); + } + + public function testAccessResourceWithJwtAccessToken() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $server->handleTokenRequest($request, $response = new Response()); + $this->assertNotNull($JwtAccessToken = $response->getParameter('access_token')); + + // make a call to the resource server using the crypto token + $request = TestRequest::createPost(array( + 'access_token' => $JwtAccessToken, + )); + + $this->assertTrue($server->verifyResourceRequest($request)); + } + + public function testAccessResourceWithJwtAccessTokenUsingSecondaryStorage() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $server->handleTokenRequest($request, $response = new Response()); + $this->assertNotNull($JwtAccessToken = $response->getParameter('access_token')); + + // make a call to the resource server using the crypto token + $request = TestRequest::createPost(array( + 'access_token' => $JwtAccessToken, + )); + + // create a resource server with the "memory" storage from the grant server + $resourceServer = new Server($server->getStorage('client_credentials')); + + $this->assertTrue($resourceServer->verifyResourceRequest($request)); + } + + public function testJwtAccessTokenWithRefreshToken() + { + $server = $this->getTestServer(); + + // add "UserCredentials" grant type and "JwtAccessToken" response type + // and ensure "JwtAccessToken" response type has "RefreshToken" storage + $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); + $server->addGrantType(new UserCredentials($memoryStorage)); + $server->addGrantType(new RefreshToken($memoryStorage)); + $server->addResponseType(new JwtAccessToken($memoryStorage, $memoryStorage, $memoryStorage), 'token'); + + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'username' => 'test-username', // valid username + 'password' => 'testpass', // valid password + )); + + // make the call to grant a crypto token + $server->handleTokenRequest($request, $response = new Response()); + $this->assertNotNull($JwtAccessToken = $response->getParameter('access_token')); + $this->assertNotNull($refreshToken = $response->getParameter('refresh_token')); + + // decode token and make sure refresh_token isn't set + list($header, $payload, $signature) = explode('.', $JwtAccessToken); + $decodedToken = json_decode(base64_decode($payload), true); + $this->assertFalse(array_key_exists('refresh_token', $decodedToken)); + + // use the refresh token to get another access token + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => $refreshToken, + )); + + $server->handleTokenRequest($request, $response = new Response()); + $this->assertNotNull($response->getParameter('access_token')); + } + + private function getTestServer() + { + $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); + + $storage = array( + 'access_token' => new JwtAccessTokenStorage($memoryStorage), + 'client' => $memoryStorage, + 'client_credentials' => $memoryStorage, + ); + $server = new Server($storage); + $server->addGrantType(new ClientCredentials($memoryStorage)); + + // make the "token" response type a JwtAccessToken + $config = array('issuer' => 'https://api.example.com'); + $server->addResponseType(new JwtAccessToken($memoryStorage, $memoryStorage, null, $config)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/ScopeTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ScopeTest.php new file mode 100644 index 0000000..99f9cf6 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ScopeTest.php @@ -0,0 +1,42 @@ +assertFalse($scopeUtil->checkScope('invalid', 'list of scopes')); + $this->assertTrue($scopeUtil->checkScope('valid', 'valid and-some other-scopes')); + $this->assertTrue($scopeUtil->checkScope('valid another-valid', 'valid another-valid and-some other-scopes')); + // all scopes must match + $this->assertFalse($scopeUtil->checkScope('valid invalid', 'valid and-some other-scopes')); + $this->assertFalse($scopeUtil->checkScope('valid valid2 invalid', 'valid valid2 and-some other-scopes')); + } + + public function testScopeStorage() + { + $scopeUtil = new Scope(); + $this->assertEquals($scopeUtil->getDefaultScope(), null); + + $scopeUtil = new Scope(array( + 'default_scope' => 'default', + 'supported_scopes' => array('this', 'that', 'another'), + )); + $this->assertEquals($scopeUtil->getDefaultScope(), 'default'); + $this->assertTrue($scopeUtil->scopeExists('this that another', 'client_id')); + + $memoryStorage = new Memory(array( + 'default_scope' => 'base', + 'supported_scopes' => array('only-this-one'), + )); + $scopeUtil = new Scope($memoryStorage); + + $this->assertEquals($scopeUtil->getDefaultScope(), 'base'); + $this->assertTrue($scopeUtil->scopeExists('only-this-one', 'client_id')); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/ServerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ServerTest.php new file mode 100644 index 0000000..747e120 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ServerTest.php @@ -0,0 +1,684 @@ +getAuthorizeController(); + } + + /** + * @expectedException LogicException OAuth2\Storage\AccessTokenInterface + **/ + public function testGetAuthorizeControllerWithNoAccessTokenStorageThrowsException() + { + // must set AccessToken or AuthorizationCode + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); + $server->getAuthorizeController(); + } + + public function testGetAuthorizeControllerWithClientStorageAndAccessTokenResponseType() + { + // must set AccessToken or AuthorizationCode + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); + $server->addResponseType($this->getMock('OAuth2\ResponseType\AccessTokenInterface')); + + $this->assertNotNull($server->getAuthorizeController()); + } + + public function testGetAuthorizeControllerWithClientStorageAndAuthorizationCodeResponseType() + { + // must set AccessToken or AuthorizationCode + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); + $server->addResponseType($this->getMock('OAuth2\ResponseType\AuthorizationCodeInterface')); + + $this->assertNotNull($server->getAuthorizeController()); + } + + /** + * @expectedException LogicException allow_implicit + **/ + public function testGetAuthorizeControllerWithClientStorageAndAccessTokenStorageThrowsException() + { + // must set AuthorizationCode or AccessToken / implicit + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); + $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface')); + + $this->assertNotNull($server->getAuthorizeController()); + } + + public function testGetAuthorizeControllerWithClientStorageAndAccessTokenStorage() + { + // must set AuthorizationCode or AccessToken / implicit + $server = new Server(array(), array('allow_implicit' => true)); + $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); + $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface')); + + $this->assertNotNull($server->getAuthorizeController()); + } + + public function testGetAuthorizeControllerWithClientStorageAndAuthorizationCodeStorage() + { + // must set AccessToken or AuthorizationCode + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); + $server->addStorage($this->getMock('OAuth2\Storage\AuthorizationCodeInterface')); + + $this->assertNotNull($server->getAuthorizeController()); + } + + /** + * @expectedException LogicException grant_types + **/ + public function testGetTokenControllerWithGrantTypeStorageThrowsException() + { + $server = new Server(); + $server->getTokenController(); + } + + /** + * @expectedException LogicException OAuth2\Storage\ClientCredentialsInterface + **/ + public function testGetTokenControllerWithNoClientCredentialsStorageThrowsException() + { + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\UserCredentialsInterface')); + $server->getTokenController(); + } + + /** + * @expectedException LogicException OAuth2\Storage\AccessTokenInterface + **/ + public function testGetTokenControllerWithNoAccessTokenStorageThrowsException() + { + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); + $server->getTokenController(); + } + + public function testGetTokenControllerWithAccessTokenAndClientCredentialsStorage() + { + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface')); + $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); + $server->getTokenController(); + } + + public function testGetTokenControllerAccessTokenStorageAndClientCredentialsStorageAndGrantTypes() + { + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface')); + $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); + $server->addGrantType($this->getMockBuilder('OAuth2\GrantType\AuthorizationCode')->disableOriginalConstructor()->getMock()); + $server->getTokenController(); + } + + /** + * @expectedException LogicException OAuth2\Storage\AccessTokenInterface + **/ + public function testGetResourceControllerWithNoAccessTokenStorageThrowsException() + { + $server = new Server(); + $server->getResourceController(); + } + + public function testGetResourceControllerWithAccessTokenStorage() + { + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface')); + $server->getResourceController(); + } + + /** + * @expectedException InvalidArgumentException OAuth2\Storage\AccessTokenInterface + **/ + public function testAddingStorageWithInvalidClass() + { + $server = new Server(); + $server->addStorage(new \StdClass()); + } + + /** + * @expectedException InvalidArgumentException access_token + **/ + public function testAddingStorageWithInvalidKey() + { + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface'), 'nonexistant_storage'); + } + + /** + * @expectedException InvalidArgumentException OAuth2\Storage\AuthorizationCodeInterface + **/ + public function testAddingStorageWithInvalidKeyStorageCombination() + { + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface'), 'authorization_code'); + } + + public function testAddingStorageWithValidKeyOnlySetsThatKey() + { + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\Memory'), 'access_token'); + + $reflection = new \ReflectionClass($server); + $prop = $reflection->getProperty('storages'); + $prop->setAccessible(true); + + $storages = $prop->getValue($server); // get the private "storages" property + + $this->assertEquals(1, count($storages)); + $this->assertTrue(isset($storages['access_token'])); + $this->assertFalse(isset($storages['authorization_code'])); + } + + public function testAddingClientStorageSetsClientCredentialsStorageByDefault() + { + $server = new Server(); + $memory = $this->getMock('OAuth2\Storage\Memory'); + $server->addStorage($memory, 'client'); + + $client_credentials = $server->getStorage('client_credentials'); + + $this->assertNotNull($client_credentials); + $this->assertEquals($client_credentials, $memory); + } + + public function testAddStorageWithNullValue() + { + $memory = $this->getMock('OAuth2\Storage\Memory'); + $server = new Server($memory); + $server->addStorage(null, 'refresh_token'); + + $client_credentials = $server->getStorage('client_credentials'); + + $this->assertNotNull($client_credentials); + $this->assertEquals($client_credentials, $memory); + + $refresh_token = $server->getStorage('refresh_token'); + + $this->assertNull($refresh_token); + } + + public function testNewServerWithNullStorageValue() + { + $memory = $this->getMock('OAuth2\Storage\Memory'); + $server = new Server(array( + 'client_credentials' => $memory, + 'refresh_token' => null, + )); + + $client_credentials = $server->getStorage('client_credentials'); + + $this->assertNotNull($client_credentials); + $this->assertEquals($client_credentials, $memory); + + $refresh_token = $server->getStorage('refresh_token'); + + $this->assertNull($refresh_token); + } + + public function testAddingClientCredentialsStorageSetsClientStorageByDefault() + { + $server = new Server(); + $memory = $this->getMock('OAuth2\Storage\Memory'); + $server->addStorage($memory, 'client_credentials'); + + $client = $server->getStorage('client'); + + $this->assertNotNull($client); + $this->assertEquals($client, $memory); + } + + public function testSettingClientStorageByDefaultDoesNotOverrideSetStorage() + { + $server = new Server(); + $pdo = $this->getMockBuilder('OAuth2\Storage\Pdo') + ->disableOriginalConstructor()->getMock(); + + $memory = $this->getMock('OAuth2\Storage\Memory'); + + $server->addStorage($pdo, 'client'); + $server->addStorage($memory, 'client_credentials'); + + $client = $server->getStorage('client'); + $client_credentials = $server->getStorage('client_credentials'); + + $this->assertEquals($client, $pdo); + $this->assertEquals($client_credentials, $memory); + } + + public function testAddingResponseType() + { + $storage = $this->getMock('OAuth2\Storage\Memory'); + $storage + ->expects($this->any()) + ->method('getClientDetails') + ->will($this->returnValue(array('client_id' => 'some_client'))); + $storage + ->expects($this->any()) + ->method('checkRestrictedGrantType') + ->will($this->returnValue(true)); + + // add with the "code" key explicitly set + $codeType = new AuthorizationCode($storage); + $server = new Server(); + $server->addStorage($storage); + $server->addResponseType($codeType); + $request = new Request(array( + 'response_type' => 'code', + 'client_id' => 'some_client', + 'redirect_uri' => 'http://example.com', + 'state' => 'xyx', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + // the response is successful + $this->assertEquals($response->getStatusCode(), 302); + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['query'], $query); + $this->assertTrue(isset($query['code'])); + $this->assertFalse(isset($query['error'])); + + // add with the "code" key not set + $codeType = new AuthorizationCode($storage); + $server = new Server(array($storage), array(), array(), array($codeType)); + $request = new Request(array( + 'response_type' => 'code', + 'client_id' => 'some_client', + 'redirect_uri' => 'http://example.com', + 'state' => 'xyx', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + // the response is successful + $this->assertEquals($response->getStatusCode(), 302); + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['query'], $query); + $this->assertTrue(isset($query['code'])); + $this->assertFalse(isset($query['error'])); + } + + public function testCustomClientAssertionType() + { + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', + 'client_id' =>'Test Client ID', + 'code' => 'testcode', + )); + // verify the mock clientAssertionType was called as expected + $clientAssertionType = $this->getMock('OAuth2\ClientAssertionType\ClientAssertionTypeInterface', array('validateRequest', 'getClientId')); + $clientAssertionType + ->expects($this->once()) + ->method('validateRequest') + ->will($this->returnValue(true)); + $clientAssertionType + ->expects($this->once()) + ->method('getClientId') + ->will($this->returnValue('Test Client ID')); + + // create mock storage + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server(array($storage), array(), array(), array(), null, null, $clientAssertionType); + $server->handleTokenRequest($request, $response = new Response()); + } + + public function testHttpBasicConfig() + { + // create mock storage + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server(array($storage), array( + 'allow_credentials_in_request_body' => false, + 'allow_public_clients' => false + )); + $server->getTokenController(); + $httpBasic = $server->getClientAssertionType(); + + $reflection = new \ReflectionClass($httpBasic); + $prop = $reflection->getProperty('config'); + $prop->setAccessible(true); + + $config = $prop->getValue($httpBasic); // get the private "config" property + + $this->assertEquals($config['allow_credentials_in_request_body'], false); + $this->assertEquals($config['allow_public_clients'], false); + } + + public function testRefreshTokenConfig() + { + // create mock storage + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server1 = new Server(array($storage)); + $server2 = new Server(array($storage), array('always_issue_new_refresh_token' => true, 'unset_refresh_token_after_use' => false)); + + $server1->getTokenController(); + $refreshToken1 = $server1->getGrantType('refresh_token'); + + $server2->getTokenController(); + $refreshToken2 = $server2->getGrantType('refresh_token'); + + $reflection1 = new \ReflectionClass($refreshToken1); + $prop1 = $reflection1->getProperty('config'); + $prop1->setAccessible(true); + + $reflection2 = new \ReflectionClass($refreshToken2); + $prop2 = $reflection2->getProperty('config'); + $prop2->setAccessible(true); + + // get the private "config" property + $config1 = $prop1->getValue($refreshToken1); + $config2 = $prop2->getValue($refreshToken2); + + $this->assertEquals($config1['always_issue_new_refresh_token'], false); + $this->assertEquals($config2['always_issue_new_refresh_token'], true); + + $this->assertEquals($config1['unset_refresh_token_after_use'], true); + $this->assertEquals($config2['unset_refresh_token_after_use'], false); + } + + /** + * Test setting "always_issue_new_refresh_token" on a server level + * + * @see test/OAuth2/GrantType/RefreshTokenTest::testValidRefreshTokenWithNewRefreshTokenInResponse + **/ + public function testValidRefreshTokenWithNewRefreshTokenInResponse() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage, array('always_issue_new_refresh_token' => true)); + + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'test-refreshtoken', // valid refresh token + )); + $token = $server->grantAccessToken($request, new Response()); + $this->assertTrue(isset($token['refresh_token']), 'refresh token should always refresh'); + + $refresh_token = $storage->getRefreshToken($token['refresh_token']); + $this->assertNotNull($refresh_token); + $this->assertEquals($refresh_token['refresh_token'], $token['refresh_token']); + $this->assertEquals($refresh_token['client_id'], $request->request('client_id')); + $this->assertTrue($token['refresh_token'] != 'test-refreshtoken', 'the refresh token returned is not the one used'); + $used_token = $storage->getRefreshToken('test-refreshtoken'); + $this->assertFalse($used_token, 'the refresh token used is no longer valid'); + } + + /** + * @expectedException InvalidArgumentException OAuth2\ResponseType\AuthorizationCodeInterface + **/ + public function testAddingUnknownResponseTypeThrowsException() + { + $server = new Server(); + $server->addResponseType($this->getMock('OAuth2\ResponseType\ResponseTypeInterface')); + } + + /** + * @expectedException LogicException OAuth2\Storage\PublicKeyInterface + **/ + public function testUsingJwtAccessTokensWithoutPublicKeyStorageThrowsException() + { + $server = new Server(array(), array('use_jwt_access_tokens' => true)); + $server->addGrantType($this->getMock('OAuth2\GrantType\GrantTypeInterface')); + $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); + $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); + + $server->getTokenController(); + } + + public function testUsingJustJwtAccessTokenStorageWithResourceControllerIsOkay() + { + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $server = new Server(array($pubkey), array('use_jwt_access_tokens' => true)); + + $this->assertNotNull($server->getResourceController()); + $this->assertInstanceOf('OAuth2\Storage\PublicKeyInterface', $server->getStorage('public_key')); + } + + /** + * @expectedException LogicException OAuth2\Storage\ClientInterface + **/ + public function testUsingJustJwtAccessTokenStorageWithAuthorizeControllerThrowsException() + { + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $server = new Server(array($pubkey), array('use_jwt_access_tokens' => true)); + $this->assertNotNull($server->getAuthorizeController()); + } + + /** + * @expectedException LogicException grant_types + **/ + public function testUsingJustJwtAccessTokenStorageWithTokenControllerThrowsException() + { + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $server = new Server(array($pubkey), array('use_jwt_access_tokens' => true)); + $server->getTokenController(); + } + + public function testUsingJwtAccessTokenAndClientStorageWithAuthorizeControllerIsOkay() + { + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $server = new Server(array($pubkey, $client), array('use_jwt_access_tokens' => true, 'allow_implicit' => true)); + $this->assertNotNull($server->getAuthorizeController()); + + $this->assertInstanceOf('OAuth2\ResponseType\JwtAccessToken', $server->getResponseType('token')); + } + + /** + * @expectedException LogicException UserClaims + **/ + public function testUsingOpenIDConnectWithoutUserClaimsThrowsException() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $server = new Server($client, array('use_openid_connect' => true)); + + $server->getAuthorizeController(); + } + + /** + * @expectedException LogicException PublicKeyInterface + **/ + public function testUsingOpenIDConnectWithoutPublicKeyThrowsException() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $userclaims = $this->getMock('OAuth2\OPenID\Storage\UserClaimsInterface'); + $server = new Server(array($client, $userclaims), array('use_openid_connect' => true)); + + $server->getAuthorizeController(); + } + + /** + * @expectedException LogicException issuer + **/ + public function testUsingOpenIDConnectWithoutIssuerThrowsException() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $server = new Server(array($client, $userclaims, $pubkey), array('use_openid_connect' => true)); + + $server->getAuthorizeController(); + } + + public function testUsingOpenIDConnectWithIssuerPublicKeyAndUserClaimsIsOkay() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $server = new Server(array($client, $userclaims, $pubkey), array( + 'use_openid_connect' => true, + 'issuer' => 'someguy', + )); + + $server->getAuthorizeController(); + + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token')); + $this->assertNull($server->getResponseType('id_token token')); + } + + /** + * @expectedException LogicException OAuth2\ResponseType\AccessTokenInterface + **/ + public function testUsingOpenIDConnectWithAllowImplicitWithoutTokenStorageThrowsException() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $server = new Server(array($client, $userclaims, $pubkey), array( + 'use_openid_connect' => true, + 'issuer' => 'someguy', + 'allow_implicit' => true, + )); + + $server->getAuthorizeController(); + } + + public function testUsingOpenIDConnectWithAllowImplicitAndUseJwtAccessTokensIsOkay() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $server = new Server(array($client, $userclaims, $pubkey), array( + 'use_openid_connect' => true, + 'issuer' => 'someguy', + 'allow_implicit' => true, + 'use_jwt_access_tokens' => true, + )); + + $server->getAuthorizeController(); + + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token')); + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenTokenInterface', $server->getResponseType('id_token token')); + } + + public function testUsingOpenIDConnectWithAllowImplicitAndAccessTokenStorageIsOkay() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $token = $this->getMock('OAuth2\Storage\AccessTokenInterface'); + $server = new Server(array($client, $userclaims, $pubkey, $token), array( + 'use_openid_connect' => true, + 'issuer' => 'someguy', + 'allow_implicit' => true, + )); + + $server->getAuthorizeController(); + + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token')); + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenTokenInterface', $server->getResponseType('id_token token')); + } + + public function testUsingOpenIDConnectWithAllowImplicitAndAccessTokenResponseTypeIsOkay() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + // $token = $this->getMock('OAuth2\Storage\AccessTokenInterface'); + $server = new Server(array($client, $userclaims, $pubkey), array( + 'use_openid_connect' => true, + 'issuer' => 'someguy', + 'allow_implicit' => true, + )); + + $token = $this->getMock('OAuth2\ResponseType\AccessTokenInterface'); + $server->addResponseType($token, 'token'); + + $server->getAuthorizeController(); + + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token')); + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenTokenInterface', $server->getResponseType('id_token token')); + } + + /** + * @expectedException LogicException OAuth2\OpenID\Storage\AuthorizationCodeInterface + **/ + public function testUsingOpenIDConnectWithAuthorizationCodeStorageThrowsException() + { + $client = $this->getMock('OAuth2\Storage\ClientCredentialsInterface'); + $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $token = $this->getMock('OAuth2\Storage\AccessTokenInterface'); + $authcode = $this->getMock('OAuth2\Storage\AuthorizationCodeInterface'); + + $server = new Server(array($client, $userclaims, $pubkey, $token, $authcode), array( + 'use_openid_connect' => true, + 'issuer' => 'someguy' + )); + + $server->getTokenController(); + + $this->assertInstanceOf('OAuth2\OpenID\GrantType\AuthorizationCode', $server->getGrantType('authorization_code')); + } + + public function testUsingOpenIDConnectWithOpenIDAuthorizationCodeStorageCreatesOpenIDAuthorizationCodeGrantType() + { + $client = $this->getMock('OAuth2\Storage\ClientCredentialsInterface'); + $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $token = $this->getMock('OAuth2\Storage\AccessTokenInterface'); + $authcode = $this->getMock('OAuth2\OpenID\Storage\AuthorizationCodeInterface'); + + $server = new Server(array($client, $userclaims, $pubkey, $token, $authcode), array( + 'use_openid_connect' => true, + 'issuer' => 'someguy' + )); + + $server->getTokenController(); + + $this->assertInstanceOf('OAuth2\OpenID\GrantType\AuthorizationCode', $server->getGrantType('authorization_code')); + } + + public function testMultipleValuedResponseTypeOrderDoesntMatter() + { + $responseType = $this->getMock('OAuth2\OpenID\ResponseType\IdTokenTokenInterface'); + $server = new Server(array(), array(), array(), array( + 'token id_token' => $responseType, + )); + + $this->assertEquals($responseType, $server->getResponseType('id_token token')); + } + + public function testAddGrantTypeWithoutKey() + { + $server = new Server(); + $server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->getMock('OAuth2\Storage\AuthorizationCodeInterface'))); + + $grantTypes = $server->getGrantTypes(); + $this->assertEquals('authorization_code', key($grantTypes)); + } + + public function testAddGrantTypeWithKey() + { + $server = new Server(); + $server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->getMock('OAuth2\Storage\AuthorizationCodeInterface')), 'ac'); + + $grantTypes = $server->getGrantTypes(); + $this->assertEquals('ac', key($grantTypes)); + } + + public function testAddGrantTypeWithKeyNotString() + { + $server = new Server(); + $server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->getMock('OAuth2\Storage\AuthorizationCodeInterface')), 42); + + $grantTypes = $server->getGrantTypes(); + $this->assertEquals('authorization_code', key($grantTypes)); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/AccessTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/AccessTokenTest.php new file mode 100644 index 0000000..345daae --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/AccessTokenTest.php @@ -0,0 +1,82 @@ +markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // assert token we are about to add does not exist + $token = $storage->getAccessToken('newtoken'); + $this->assertFalse($token); + + // add new token + $expires = time() + 20; + $success = $storage->setAccessToken('newtoken', 'client ID', 'SOMEUSERID', $expires); + $this->assertTrue($success); + + $token = $storage->getAccessToken('newtoken'); + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('client_id', $token); + $this->assertArrayHasKey('user_id', $token); + $this->assertArrayHasKey('expires', $token); + $this->assertEquals($token['access_token'], 'newtoken'); + $this->assertEquals($token['client_id'], 'client ID'); + $this->assertEquals($token['user_id'], 'SOMEUSERID'); + $this->assertEquals($token['expires'], $expires); + + // change existing token + $expires = time() + 42; + $success = $storage->setAccessToken('newtoken', 'client ID2', 'SOMEOTHERID', $expires); + $this->assertTrue($success); + + $token = $storage->getAccessToken('newtoken'); + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('client_id', $token); + $this->assertArrayHasKey('user_id', $token); + $this->assertArrayHasKey('expires', $token); + $this->assertEquals($token['access_token'], 'newtoken'); + $this->assertEquals($token['client_id'], 'client ID2'); + $this->assertEquals($token['user_id'], 'SOMEOTHERID'); + $this->assertEquals($token['expires'], $expires); + + // add token with scope having an empty string value + $expires = time() + 42; + $success = $storage->setAccessToken('newtoken', 'client ID', 'SOMEOTHERID', $expires, ''); + $this->assertTrue($success); + } + + /** @dataProvider provideStorage */ + public function testUnsetAccessToken(AccessTokenInterface $storage) + { + if ($storage instanceof NullStorage || !method_exists($storage, 'unsetAccessToken')) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // assert token we are unset does not exist + $token = $storage->getAccessToken('revokabletoken'); + $this->assertFalse($token); + + // add new token + $expires = time() + 20; + $success = $storage->setAccessToken('revokabletoken', 'client ID', 'SOMEUSERID', $expires); + $this->assertTrue($success); + + $storage->unsetAccessToken('revokabletoken'); + + // assert token we are unset does not exist + $token = $storage->getAccessToken('revokabletoken'); + $this->assertFalse($token); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/AuthorizationCodeTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/AuthorizationCodeTest.php new file mode 100644 index 0000000..2d901b5 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/AuthorizationCodeTest.php @@ -0,0 +1,106 @@ +markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // nonexistant client_id + $details = $storage->getAuthorizationCode('faketoken'); + $this->assertFalse($details); + + // valid client_id + $details = $storage->getAuthorizationCode('testtoken'); + $this->assertNotNull($details); + } + + /** @dataProvider provideStorage */ + public function testSetAuthorizationCode(AuthorizationCodeInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // assert code we are about to add does not exist + $code = $storage->getAuthorizationCode('newcode'); + $this->assertFalse($code); + + // add new code + $expires = time() + 20; + $success = $storage->setAuthorizationCode('newcode', 'client ID', 'SOMEUSERID', 'http://example.com', $expires); + $this->assertTrue($success); + + $code = $storage->getAuthorizationCode('newcode'); + $this->assertNotNull($code); + $this->assertArrayHasKey('authorization_code', $code); + $this->assertArrayHasKey('client_id', $code); + $this->assertArrayHasKey('user_id', $code); + $this->assertArrayHasKey('redirect_uri', $code); + $this->assertArrayHasKey('expires', $code); + $this->assertEquals($code['authorization_code'], 'newcode'); + $this->assertEquals($code['client_id'], 'client ID'); + $this->assertEquals($code['user_id'], 'SOMEUSERID'); + $this->assertEquals($code['redirect_uri'], 'http://example.com'); + $this->assertEquals($code['expires'], $expires); + + // change existing code + $expires = time() + 42; + $success = $storage->setAuthorizationCode('newcode', 'client ID2', 'SOMEOTHERID', 'http://example.org', $expires); + $this->assertTrue($success); + + $code = $storage->getAuthorizationCode('newcode'); + $this->assertNotNull($code); + $this->assertArrayHasKey('authorization_code', $code); + $this->assertArrayHasKey('client_id', $code); + $this->assertArrayHasKey('user_id', $code); + $this->assertArrayHasKey('redirect_uri', $code); + $this->assertArrayHasKey('expires', $code); + $this->assertEquals($code['authorization_code'], 'newcode'); + $this->assertEquals($code['client_id'], 'client ID2'); + $this->assertEquals($code['user_id'], 'SOMEOTHERID'); + $this->assertEquals($code['redirect_uri'], 'http://example.org'); + $this->assertEquals($code['expires'], $expires); + + // add new code with scope having an empty string value + $expires = time() + 20; + $success = $storage->setAuthorizationCode('newcode', 'client ID', 'SOMEUSERID', 'http://example.com', $expires, ''); + $this->assertTrue($success); + } + + /** @dataProvider provideStorage */ + public function testExpireAccessToken(AccessTokenInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // create a valid code + $expires = time() + 20; + $success = $storage->setAuthorizationCode('code-to-expire', 'client ID', 'SOMEUSERID', 'http://example.com', time() + 20); + $this->assertTrue($success); + + // verify the new code exists + $code = $storage->getAuthorizationCode('code-to-expire'); + $this->assertNotNull($code); + + $this->assertArrayHasKey('authorization_code', $code); + $this->assertEquals($code['authorization_code'], 'code-to-expire'); + + // now expire the code and ensure it's no longer available + $storage->expireAuthorizationCode('code-to-expire'); + $code = $storage->getAuthorizationCode('code-to-expire'); + $this->assertFalse($code); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ClientCredentialsTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ClientCredentialsTest.php new file mode 100644 index 0000000..15289af --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ClientCredentialsTest.php @@ -0,0 +1,28 @@ +markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // nonexistant client_id + $pass = $storage->checkClientCredentials('fakeclient', 'testpass'); + $this->assertFalse($pass); + + // invalid password + $pass = $storage->checkClientCredentials('oauth_test_client', 'invalidcredentials'); + $this->assertFalse($pass); + + // valid credentials + $pass = $storage->checkClientCredentials('oauth_test_client', 'testpass'); + $this->assertTrue($pass); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ClientTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ClientTest.php new file mode 100644 index 0000000..d4991c1 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ClientTest.php @@ -0,0 +1,87 @@ +markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // nonexistant client_id + $details = $storage->getClientDetails('fakeclient'); + $this->assertFalse($details); + + // valid client_id + $details = $storage->getClientDetails('oauth_test_client'); + $this->assertNotNull($details); + $this->assertArrayHasKey('client_id', $details); + $this->assertArrayHasKey('client_secret', $details); + $this->assertArrayHasKey('redirect_uri', $details); + } + + /** @dataProvider provideStorage */ + public function testCheckRestrictedGrantType(ClientInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // Check invalid + $pass = $storage->checkRestrictedGrantType('oauth_test_client', 'authorization_code'); + $this->assertFalse($pass); + + // Check valid + $pass = $storage->checkRestrictedGrantType('oauth_test_client', 'implicit'); + $this->assertTrue($pass); + } + + /** @dataProvider provideStorage */ + public function testGetAccessToken(ClientInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // nonexistant client_id + $details = $storage->getAccessToken('faketoken'); + $this->assertFalse($details); + + // valid client_id + $details = $storage->getAccessToken('testtoken'); + $this->assertNotNull($details); + } + + /** @dataProvider provideStorage */ + public function testSaveClient(ClientInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + $clientId = 'some-client-'.rand(); + + // create a new client + $success = $storage->setClientDetails($clientId, 'somesecret', 'http://test.com', 'client_credentials', 'clientscope1', 'brent@brentertainment.com'); + $this->assertTrue($success); + + // valid client_id + $details = $storage->getClientDetails($clientId); + $this->assertEquals($details['client_secret'], 'somesecret'); + $this->assertEquals($details['redirect_uri'], 'http://test.com'); + $this->assertEquals($details['grant_types'], 'client_credentials'); + $this->assertEquals($details['scope'], 'clientscope1'); + $this->assertEquals($details['user_id'], 'brent@brentertainment.com'); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/DynamoDBTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/DynamoDBTest.php new file mode 100644 index 0000000..2147f09 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/DynamoDBTest.php @@ -0,0 +1,40 @@ +getMockBuilder('\Aws\DynamoDb\DynamoDbClient') + ->disableOriginalConstructor() + ->setMethods(array('query')) + ->getMock(); + + $return = $this->getMockBuilder('\Guzzle\Service\Resource\Model') + ->setMethods(array('count', 'toArray')) + ->getMock(); + + $data = array( + 'Items' => array(), + 'Count' => 0, + 'ScannedCount'=> 0 + ); + + $return->expects($this->once()) + ->method('count') + ->will($this->returnValue(count($data))); + + $return->expects($this->once()) + ->method('toArray') + ->will($this->returnValue($data)); + + // should return null default scope if none is set in database + $client->expects($this->once()) + ->method('query') + ->will($this->returnValue($return)); + + $storage = new DynamoDB($client); + $this->assertNull($storage->getDefaultScope()); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/JwtAccessTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/JwtAccessTokenTest.php new file mode 100644 index 0000000..a6acbea --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/JwtAccessTokenTest.php @@ -0,0 +1,41 @@ +getMemoryStorage(); + $encryptionUtil = new Jwt(); + + $jwtAccessToken = array( + 'access_token' => rand(), + 'expires' => time() + 100, + 'scope' => 'foo', + ); + + $token = $encryptionUtil->encode($jwtAccessToken, $storage->getPrivateKey(), $storage->getEncryptionAlgorithm()); + + $this->assertNotNull($token); + + $tokenData = $crypto->getAccessToken($token); + + $this->assertTrue(is_array($tokenData)); + + /* assert the decoded token is the same */ + $this->assertEquals($tokenData['access_token'], $jwtAccessToken['access_token']); + $this->assertEquals($tokenData['expires'], $jwtAccessToken['expires']); + $this->assertEquals($tokenData['scope'], $jwtAccessToken['scope']); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/JwtBearerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/JwtBearerTest.php new file mode 100644 index 0000000..d0ab9b8 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/JwtBearerTest.php @@ -0,0 +1,25 @@ +markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // nonexistant client_id + $key = $storage->getClientKey('this-is-not-real', 'nor-is-this'); + $this->assertFalse($key); + + // valid client_id and subject + $key = $storage->getClientKey('oauth_test_client', 'test_subject'); + $this->assertNotNull($key); + $this->assertEquals($key, Bootstrap::getInstance()->getTestPublicKey()); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/PdoTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/PdoTest.php new file mode 100644 index 0000000..57eb390 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/PdoTest.php @@ -0,0 +1,39 @@ +getSqliteDir())); + $storage = new Pdo($pdo); + + $this->assertNotNull($storage->getClientDetails('oauth_test_client')); + } + + public function testCreatePdoStorageUsingDSN() + { + $dsn = sprintf('sqlite://%s', Bootstrap::getInstance()->getSqliteDir()); + $storage = new Pdo($dsn); + + $this->assertNotNull($storage->getClientDetails('oauth_test_client')); + } + + public function testCreatePdoStorageUsingConfig() + { + $config = array('dsn' => sprintf('sqlite://%s', Bootstrap::getInstance()->getSqliteDir())); + $storage = new Pdo($config); + + $this->assertNotNull($storage->getClientDetails('oauth_test_client')); + } + + /** + * @expectedException InvalidArgumentException dsn + */ + public function testCreatePdoStorageWithoutDSNThrowsException() + { + $config = array('username' => 'brent', 'password' => 'brentisaballer'); + $storage = new Pdo($config); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/PublicKeyTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/PublicKeyTest.php new file mode 100644 index 0000000..f851958 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/PublicKeyTest.php @@ -0,0 +1,29 @@ +markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + if (!$storage instanceof PublicKeyInterface) { + // incompatible storage + return; + } + + $configDir = Bootstrap::getInstance()->getConfigDir(); + $globalPublicKey = file_get_contents($configDir.'/keys/id_rsa.pub'); + $globalPrivateKey = file_get_contents($configDir.'/keys/id_rsa'); + + /* assert values from storage */ + $this->assertEquals($storage->getPublicKey(), $globalPublicKey); + $this->assertEquals($storage->getPrivateKey(), $globalPrivateKey); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/RefreshTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/RefreshTokenTest.php new file mode 100644 index 0000000..314c931 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/RefreshTokenTest.php @@ -0,0 +1,41 @@ +markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // assert token we are about to add does not exist + $token = $storage->getRefreshToken('refreshtoken'); + $this->assertFalse($token); + + // add new token + $expires = time() + 20; + $success = $storage->setRefreshToken('refreshtoken', 'client ID', 'SOMEUSERID', $expires); + $this->assertTrue($success); + + $token = $storage->getRefreshToken('refreshtoken'); + $this->assertNotNull($token); + $this->assertArrayHasKey('refresh_token', $token); + $this->assertArrayHasKey('client_id', $token); + $this->assertArrayHasKey('user_id', $token); + $this->assertArrayHasKey('expires', $token); + $this->assertEquals($token['refresh_token'], 'refreshtoken'); + $this->assertEquals($token['client_id'], 'client ID'); + $this->assertEquals($token['user_id'], 'SOMEUSERID'); + $this->assertEquals($token['expires'], $expires); + + // add token with scope having an empty string value + $expires = time() + 20; + $success = $storage->setRefreshToken('refreshtoken2', 'client ID', 'SOMEUSERID', $expires, ''); + $this->assertTrue($success); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ScopeTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ScopeTest.php new file mode 100644 index 0000000..fd1edeb --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ScopeTest.php @@ -0,0 +1,53 @@ +markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + if (!$storage instanceof ScopeInterface) { + // incompatible storage + return; + } + + //Test getting scopes + $scopeUtil = new Scope($storage); + $this->assertTrue($scopeUtil->scopeExists('supportedscope1')); + $this->assertTrue($scopeUtil->scopeExists('supportedscope1 supportedscope2 supportedscope3')); + $this->assertFalse($scopeUtil->scopeExists('fakescope')); + $this->assertFalse($scopeUtil->scopeExists('supportedscope1 supportedscope2 supportedscope3 fakescope')); + } + + /** @dataProvider provideStorage */ + public function testGetDefaultScope($storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + if (!$storage instanceof ScopeInterface) { + // incompatible storage + return; + } + + // test getting default scope + $scopeUtil = new Scope($storage); + $expected = explode(' ', $scopeUtil->getDefaultScope()); + $actual = explode(' ', 'defaultscope1 defaultscope2'); + sort($expected); + sort($actual); + $this->assertEquals($expected, $actual); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/UserCredentialsTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/UserCredentialsTest.php new file mode 100644 index 0000000..65655a6 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/UserCredentialsTest.php @@ -0,0 +1,40 @@ +markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // create a new user for testing + $success = $storage->setUser('testusername', 'testpass', 'Test', 'User'); + $this->assertTrue($success); + + // correct credentials + $this->assertTrue($storage->checkUserCredentials('testusername', 'testpass')); + // invalid password + $this->assertFalse($storage->checkUserCredentials('testusername', 'fakepass')); + // invalid username + $this->assertFalse($storage->checkUserCredentials('fakeusername', 'testpass')); + + // invalid username + $this->assertFalse($storage->getUserDetails('fakeusername')); + + // ensure all properties are set + $user = $storage->getUserDetails('testusername'); + $this->assertTrue($user !== false); + $this->assertArrayHasKey('user_id', $user); + $this->assertArrayHasKey('first_name', $user); + $this->assertArrayHasKey('last_name', $user); + $this->assertEquals($user['user_id'], 'testusername'); + $this->assertEquals($user['first_name'], 'Test'); + $this->assertEquals($user['last_name'], 'User'); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/TokenType/BearerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/TokenType/BearerTest.php new file mode 100644 index 0000000..a2e000e --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/TokenType/BearerTest.php @@ -0,0 +1,58 @@ + 'ThisIsMyAccessToken' + )); + $request->server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=UTF-8'; + + $param = $bearer->getAccessTokenParameter($request, $response = new Response()); + $this->assertEquals($param, 'ThisIsMyAccessToken'); + } + + public function testInvalidContentType() + { + $bearer = new Bearer(); + $request = TestRequest::createPost(array( + 'access_token' => 'ThisIsMyAccessToken' + )); + $request->server['CONTENT_TYPE'] = 'application/json; charset=UTF-8'; + + $param = $bearer->getAccessTokenParameter($request, $response = new Response()); + $this->assertNull($param); + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'The content type for POST requests must be "application/x-www-form-urlencoded"'); + } + + public function testValidRequestUsingAuthorizationHeader() + { + $bearer = new Bearer(); + $request = new TestRequest(); + $request->headers['AUTHORIZATION'] = 'Bearer MyToken'; + $request->server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=UTF-8'; + + $param = $bearer->getAccessTokenParameter($request, $response = new Response()); + $this->assertEquals('MyToken', $param); + } + + public function testValidRequestUsingAuthorizationHeaderCaseInsensitive() + { + $bearer = new Bearer(); + $request = new TestRequest(); + $request->server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=UTF-8'; + $request->headers['Authorization'] = 'Bearer MyToken'; + + $param = $bearer->getAccessTokenParameter($request, $response = new Response()); + $this->assertEquals('MyToken', $param); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/bootstrap.php b/vendor/bshaffer/oauth2-server-php/test/bootstrap.php new file mode 100644 index 0000000..0a4af07 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/bootstrap.php @@ -0,0 +1,12 @@ +cleanupTravisDynamoDb(); diff --git a/vendor/bshaffer/oauth2-server-php/test/config/keys/id_rsa b/vendor/bshaffer/oauth2-server-php/test/config/keys/id_rsa new file mode 100644 index 0000000..e8b9eff --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/config/keys/id_rsa @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQC8fpi06NfVYHAOAnxNMVnTXr/ptsLsNjP+uAt2eO0cc5J9H5XV +8lFVujOrRu/JWi1TDmAvOaf/6A3BphIA1Pwp0AAqlZdwizIum8j0KzpsGYH5qReN +QDwF3oUSKMsQCCGCDHrDYifG/pRi9bN1ZVjEXPr35HJuBe+FQpZTs8DewwIDAQAB +AoGARfNxNknmtx/n1bskZ/01iZRzAge6BLEE0LV6Q4gS7mkRZu/Oyiv39Sl5vUlA ++WdGxLjkBwKNjxGN8Vxw9/ASd8rSsqeAUYIwAeifXrHhj5DBPQT/pDPkeFnp9B1w +C6jo+3AbBQ4/b0ONSIEnCL2xGGglSIAxO17T1ViXp7lzXPECQQDe63nkRdWM0OCb +oaHQPT3E26224maIstrGFUdt9yw3yJf4bOF7TtiPLlLuHsTTge3z+fG6ntC0xG56 +1cl37C3ZAkEA2HdVcRGugNp/qmVz4LJTpD+WZKi73PLAO47wDOrYh9Pn2I6fcEH0 +CPnggt1ko4ujvGzFTvRH64HXa6aPCv1j+wJBAMQMah3VQPNf/DlDVFEUmw9XeBZg +VHaifX851aEjgXLp6qVj9IYCmLiLsAmVa9rr6P7p8asD418nZlaHUHE0eDkCQQCr +uxis6GMx1Ka971jcJX2X696LoxXPd0KsvXySMupv79yagKPa8mgBiwPjrnK+EPVo +cj6iochA/bSCshP/mwFrAkBHEKPi6V6gb94JinCT7x3weahbdp6bJ6/nzBH/p9VA +HoT1JtwNFhGv9BCjmDydshQHfSWpY9NxlccBKL7ITm8R +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/vendor/bshaffer/oauth2-server-php/test/config/keys/id_rsa.pub b/vendor/bshaffer/oauth2-server-php/test/config/keys/id_rsa.pub new file mode 100644 index 0000000..1ac15f5 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/config/keys/id_rsa.pub @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICiDCCAfGgAwIBAgIBADANBgkqhkiG9w0BAQQFADA9MQswCQYDVQQGEwJVUzEL +MAkGA1UECBMCVVQxITAfBgNVBAoTGFZpZ25ldHRlIENvcnBvcmF0aW9uIFNCWDAe +Fw0xMTEwMTUwMzE4MjdaFw0zMTEwMTAwMzE4MjdaMD0xCzAJBgNVBAYTAlVTMQsw +CQYDVQQIEwJVVDEhMB8GA1UEChMYVmlnbmV0dGUgQ29ycG9yYXRpb24gU0JYMIGf +MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8fpi06NfVYHAOAnxNMVnTXr/ptsLs +NjP+uAt2eO0cc5J9H5XV8lFVujOrRu/JWi1TDmAvOaf/6A3BphIA1Pwp0AAqlZdw +izIum8j0KzpsGYH5qReNQDwF3oUSKMsQCCGCDHrDYifG/pRi9bN1ZVjEXPr35HJu +Be+FQpZTs8DewwIDAQABo4GXMIGUMB0GA1UdDgQWBBRe8hrEXm+Yim4YlD5Nx+1K +vCYs9DBlBgNVHSMEXjBcgBRe8hrEXm+Yim4YlD5Nx+1KvCYs9KFBpD8wPTELMAkG +A1UEBhMCVVMxCzAJBgNVBAgTAlVUMSEwHwYDVQQKExhWaWduZXR0ZSBDb3Jwb3Jh +dGlvbiBTQliCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQBjhyRD +lM7vnLn6drgQVftW5V9nDFAyPAuiGvMIKFSbiAf1PxXCRn5sfJquwWKsJUi4ZGNl +aViXdFmN6/F13PSM+yg63tpKy0fYqMbTM+Oe5WuSHkSW1VuYNHV+24adgNk/FRDL +FRrlM1f6s9VTLWvwGItjfrof0Ba8Uq7ZDSb9Xg== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/vendor/bshaffer/oauth2-server-php/test/config/storage.json b/vendor/bshaffer/oauth2-server-php/test/config/storage.json new file mode 100644 index 0000000..2d43c07 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/config/storage.json @@ -0,0 +1,177 @@ +{ + "authorization_codes": { + "testcode": { + "client_id": "Test Client ID", + "user_id": "", + "redirect_uri": "", + "expires": "9999999999", + "id_token": "IDTOKEN" + }, + "testcode-with-scope": { + "client_id": "Test Client ID", + "user_id": "", + "redirect_uri": "", + "expires": "9999999999", + "scope": "scope1 scope2" + }, + "testcode-expired": { + "client_id": "Test Client ID", + "user_id": "", + "redirect_uri": "", + "expires": "1356998400" + }, + "testcode-empty-secret": { + "client_id": "Test Client ID Empty Secret", + "user_id": "", + "redirect_uri": "", + "expires": "9999999999" + }, + "testcode-openid": { + "client_id": "Test Client ID", + "user_id": "", + "redirect_uri": "", + "expires": "9999999999", + "id_token": "test_id_token" + } + }, + "client_credentials" : { + "Test Client ID": { + "client_secret": "TestSecret" + }, + "Test Client ID with Redirect Uri": { + "client_secret": "TestSecret2", + "redirect_uri": "http://brentertainment.com" + }, + "Test Client ID with Multiple Redirect Uris": { + "client_secret": "TestSecret3", + "redirect_uri": "http://brentertainment.com http://morehazards.com" + }, + "Test Client ID with Redirect Uri Parts": { + "client_secret": "TestSecret4", + "redirect_uri": "http://user:pass@brentertainment.com:2222/authorize/cb?auth_type=oauth&test=true" + }, + "Test Some Other Client": { + "client_secret": "TestSecret3" + }, + "Test Client ID Empty Secret": { + "client_secret": "" + }, + "Test Client ID For Password Grant": { + "grant_types": "password", + "client_secret": "" + }, + "Client ID With User ID": { + "client_secret": "TestSecret", + "user_id": "brent@brentertainment.com" + }, + "oauth_test_client": { + "client_secret": "testpass", + "grant_types": "implicit password" + } + }, + "user_credentials" : { + "test-username": { + "password": "testpass" + }, + "testusername": { + "password": "testpass" + }, + "testuser": { + "password": "password", + "email": "testuser@test.com", + "email_verified": true + }, + "johndoe": { + "password": "password" + } + }, + "refresh_tokens" : { + "test-refreshtoken": { + "refresh_token": "test-refreshtoken", + "client_id": "Test Client ID", + "user_id": "test-username", + "expires": 0, + "scope": null + }, + "test-refreshtoken-with-scope": { + "refresh_token": "test-refreshtoken", + "client_id": "Test Client ID", + "user_id": "test-username", + "expires": 0, + "scope": "scope1 scope2" + } + }, + "access_tokens" : { + "accesstoken-expired": { + "access_token": "accesstoken-expired", + "client_id": "Test Client ID", + "expires": 1234567, + "scope": null + }, + "accesstoken-scope": { + "access_token": "accesstoken-scope", + "client_id": "Test Client ID", + "expires": 99999999900, + "scope": "testscope" + }, + "accesstoken-openid-connect": { + "access_token": "accesstoken-openid-connect", + "client_id": "Test Client ID", + "user_id": "testuser", + "expires": 99999999900, + "scope": "openid email" + }, + "accesstoken-malformed": { + "access_token": "accesstoken-mallformed", + "expires": 99999999900, + "scope": "testscope" + } + }, + "jwt": { + "Test Client ID": { + "key": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5/SxVlE8gnpFqCxgl2wjhzY7u\ncEi00s0kUg3xp7lVEvgLgYcAnHiWp+gtSjOFfH2zsvpiWm6Lz5f743j/FEzHIO1o\nwR0p4d9pOaJK07d01+RzoQLOIQAgXrr4T1CCWUesncwwPBVCyy2Mw3Nmhmr9MrF8\nUlvdRKBxriRnlP3qJQIDAQAB\n-----END PUBLIC KEY-----", + "subject": "testuser@ourdomain.com" + }, + "Test Client ID PHP-5.2": { + "key": "mysecretkey", + "subject": "testuser@ourdomain.com" + }, + "Missing Key Client": { + "key": null, + "subject": "testuser@ourdomain.com" + }, + "Missing Key Client PHP-5.2": { + "key": null, + "subject": "testuser@ourdomain.com" + }, + "oauth_test_client": { + "key": "-----BEGIN CERTIFICATE-----\nMIICiDCCAfGgAwIBAgIBADANBgkqhkiG9w0BAQQFADA9MQswCQYDVQQGEwJVUzEL\nMAkGA1UECBMCVVQxITAfBgNVBAoTGFZpZ25ldHRlIENvcnBvcmF0aW9uIFNCWDAe\nFw0xMTEwMTUwMzE4MjdaFw0zMTEwMTAwMzE4MjdaMD0xCzAJBgNVBAYTAlVTMQsw\nCQYDVQQIEwJVVDEhMB8GA1UEChMYVmlnbmV0dGUgQ29ycG9yYXRpb24gU0JYMIGf\nMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8fpi06NfVYHAOAnxNMVnTXr/ptsLs\nNjP+uAt2eO0cc5J9H5XV8lFVujOrRu/JWi1TDmAvOaf/6A3BphIA1Pwp0AAqlZdw\nizIum8j0KzpsGYH5qReNQDwF3oUSKMsQCCGCDHrDYifG/pRi9bN1ZVjEXPr35HJu\nBe+FQpZTs8DewwIDAQABo4GXMIGUMB0GA1UdDgQWBBRe8hrEXm+Yim4YlD5Nx+1K\nvCYs9DBlBgNVHSMEXjBcgBRe8hrEXm+Yim4YlD5Nx+1KvCYs9KFBpD8wPTELMAkG\nA1UEBhMCVVMxCzAJBgNVBAgTAlVUMSEwHwYDVQQKExhWaWduZXR0ZSBDb3Jwb3Jh\ndGlvbiBTQliCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQBjhyRD\nlM7vnLn6drgQVftW5V9nDFAyPAuiGvMIKFSbiAf1PxXCRn5sfJquwWKsJUi4ZGNl\naViXdFmN6/F13PSM+yg63tpKy0fYqMbTM+Oe5WuSHkSW1VuYNHV+24adgNk/FRDL\nFRrlM1f6s9VTLWvwGItjfrof0Ba8Uq7ZDSb9Xg==\n-----END CERTIFICATE-----", + "subject": "test_subject" + } + }, + "jti": [ + { + "issuer": "Test Client ID", + "subject": "testuser@ourdomain.com", + "audience": "http://myapp.com/oauth/auth", + "expires": 99999999900, + "jti": "used_jti" + } + ], + "supported_scopes" : [ + "scope1", + "scope2", + "scope3", + "clientscope1", + "clientscope2", + "clientscope3", + "supportedscope1", + "supportedscope2", + "supportedscope3", + "supportedscope4" + ], + "keys": { + "public_key": "-----BEGIN CERTIFICATE-----\nMIICiDCCAfGgAwIBAgIBADANBgkqhkiG9w0BAQQFADA9MQswCQYDVQQGEwJVUzEL\nMAkGA1UECBMCVVQxITAfBgNVBAoTGFZpZ25ldHRlIENvcnBvcmF0aW9uIFNCWDAe\nFw0xMTEwMTUwMzE4MjdaFw0zMTEwMTAwMzE4MjdaMD0xCzAJBgNVBAYTAlVTMQsw\nCQYDVQQIEwJVVDEhMB8GA1UEChMYVmlnbmV0dGUgQ29ycG9yYXRpb24gU0JYMIGf\nMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8fpi06NfVYHAOAnxNMVnTXr/ptsLs\nNjP+uAt2eO0cc5J9H5XV8lFVujOrRu/JWi1TDmAvOaf/6A3BphIA1Pwp0AAqlZdw\nizIum8j0KzpsGYH5qReNQDwF3oUSKMsQCCGCDHrDYifG/pRi9bN1ZVjEXPr35HJu\nBe+FQpZTs8DewwIDAQABo4GXMIGUMB0GA1UdDgQWBBRe8hrEXm+Yim4YlD5Nx+1K\nvCYs9DBlBgNVHSMEXjBcgBRe8hrEXm+Yim4YlD5Nx+1KvCYs9KFBpD8wPTELMAkG\nA1UEBhMCVVMxCzAJBgNVBAgTAlVUMSEwHwYDVQQKExhWaWduZXR0ZSBDb3Jwb3Jh\ndGlvbiBTQliCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQBjhyRD\nlM7vnLn6drgQVftW5V9nDFAyPAuiGvMIKFSbiAf1PxXCRn5sfJquwWKsJUi4ZGNl\naViXdFmN6/F13PSM+yg63tpKy0fYqMbTM+Oe5WuSHkSW1VuYNHV+24adgNk/FRDL\nFRrlM1f6s9VTLWvwGItjfrof0Ba8Uq7ZDSb9Xg==\n-----END CERTIFICATE-----", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQC8fpi06NfVYHAOAnxNMVnTXr/ptsLsNjP+uAt2eO0cc5J9H5XV\n8lFVujOrRu/JWi1TDmAvOaf/6A3BphIA1Pwp0AAqlZdwizIum8j0KzpsGYH5qReN\nQDwF3oUSKMsQCCGCDHrDYifG/pRi9bN1ZVjEXPr35HJuBe+FQpZTs8DewwIDAQAB\nAoGARfNxNknmtx/n1bskZ/01iZRzAge6BLEE0LV6Q4gS7mkRZu/Oyiv39Sl5vUlA\n+WdGxLjkBwKNjxGN8Vxw9/ASd8rSsqeAUYIwAeifXrHhj5DBPQT/pDPkeFnp9B1w\nC6jo+3AbBQ4/b0ONSIEnCL2xGGglSIAxO17T1ViXp7lzXPECQQDe63nkRdWM0OCb\noaHQPT3E26224maIstrGFUdt9yw3yJf4bOF7TtiPLlLuHsTTge3z+fG6ntC0xG56\n1cl37C3ZAkEA2HdVcRGugNp/qmVz4LJTpD+WZKi73PLAO47wDOrYh9Pn2I6fcEH0\nCPnggt1ko4ujvGzFTvRH64HXa6aPCv1j+wJBAMQMah3VQPNf/DlDVFEUmw9XeBZg\nVHaifX851aEjgXLp6qVj9IYCmLiLsAmVa9rr6P7p8asD418nZlaHUHE0eDkCQQCr\nuxis6GMx1Ka971jcJX2X696LoxXPd0KsvXySMupv79yagKPa8mgBiwPjrnK+EPVo\ncj6iochA/bSCshP/mwFrAkBHEKPi6V6gb94JinCT7x3weahbdp6bJ6/nzBH/p9VA\nHoT1JtwNFhGv9BCjmDydshQHfSWpY9NxlccBKL7ITm8R\n-----END RSA PRIVATE KEY-----" + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Request/TestRequest.php b/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Request/TestRequest.php new file mode 100644 index 0000000..7bbce28 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Request/TestRequest.php @@ -0,0 +1,61 @@ +query = $_GET; + $this->request = $_POST; + $this->server = $_SERVER; + $this->headers = array(); + } + + public function query($name, $default = null) + { + return isset($this->query[$name]) ? $this->query[$name] : $default; + } + + public function request($name, $default = null) + { + return isset($this->request[$name]) ? $this->request[$name] : $default; + } + + public function server($name, $default = null) + { + return isset($this->server[$name]) ? $this->server[$name] : $default; + } + + public function getAllQueryParameters() + { + return $this->query; + } + + public function setQuery(array $query) + { + $this->query = $query; + } + + public function setPost(array $params) + { + $this->server['REQUEST_METHOD'] = 'POST'; + $this->request = $params; + } + + public static function createPost(array $params = array()) + { + $request = new self(); + $request->setPost($params); + + return $request; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/BaseTest.php b/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/BaseTest.php new file mode 100755 index 0000000..921d525 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/BaseTest.php @@ -0,0 +1,34 @@ +getMemoryStorage(); + $sqlite = Bootstrap::getInstance()->getSqlitePdo(); + $mysql = Bootstrap::getInstance()->getMysqlPdo(); + $postgres = Bootstrap::getInstance()->getPostgresPdo(); + $mongo = Bootstrap::getInstance()->getMongo(); + $redis = Bootstrap::getInstance()->getRedisStorage(); + $cassandra = Bootstrap::getInstance()->getCassandraStorage(); + $dynamodb = Bootstrap::getInstance()->getDynamoDbStorage(); + $couchbase = Bootstrap::getInstance()->getCouchbase(); + + /* hack until we can fix "default_scope" dependencies in other tests */ + $memory->defaultScope = 'defaultscope1 defaultscope2'; + + return array( + array($memory), + array($sqlite), + array($mysql), + array($postgres), + array($mongo), + array($redis), + array($cassandra), + array($dynamodb), + array($couchbase), + ); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/Bootstrap.php b/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/Bootstrap.php new file mode 100755 index 0000000..efb6644 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/Bootstrap.php @@ -0,0 +1,884 @@ +configDir = __DIR__.'/../../../config'; + } + + public static function getInstance() + { + if (!self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + public function getSqlitePdo() + { + if (!$this->sqlite) { + $this->removeSqliteDb(); + $pdo = new \PDO(sprintf('sqlite://%s', $this->getSqliteDir())); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $this->createSqliteDb($pdo); + + $this->sqlite = new Pdo($pdo); + } + + return $this->sqlite; + } + + public function getPostgresPdo() + { + if (!$this->postgres) { + if (in_array('pgsql', \PDO::getAvailableDrivers())) { + $this->removePostgresDb(); + $this->createPostgresDb(); + if ($pdo = $this->getPostgresDriver()) { + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $this->populatePostgresDb($pdo); + $this->postgres = new Pdo($pdo); + } + } else { + $this->postgres = new NullStorage('Postgres', 'Missing postgres PDO extension.'); + } + } + + return $this->postgres; + } + + public function getPostgresDriver() + { + try { + $pdo = new \PDO('pgsql:host=localhost;dbname=oauth2_server_php', 'postgres'); + + return $pdo; + } catch (\PDOException $e) { + $this->postgres = new NullStorage('Postgres', $e->getMessage()); + } + } + + public function getMemoryStorage() + { + return new Memory(json_decode(file_get_contents($this->configDir. '/storage.json'), true)); + } + + public function getRedisStorage() + { + if (!$this->redis) { + if (class_exists('Predis\Client')) { + $redis = new \Predis\Client(); + if ($this->testRedisConnection($redis)) { + $redis->flushdb(); + $this->redis = new Redis($redis); + $this->createRedisDb($this->redis); + } else { + $this->redis = new NullStorage('Redis', 'Unable to connect to redis server on port 6379'); + } + } else { + $this->redis = new NullStorage('Redis', 'Missing redis library. Please run "composer.phar require predis/predis:dev-master"'); + } + } + + return $this->redis; + } + + private function testRedisConnection(\Predis\Client $redis) + { + try { + $redis->connect(); + } catch (\Predis\CommunicationException $exception) { + // we were unable to connect to the redis server + return false; + } + + return true; + } + + public function getMysqlPdo() + { + if (!$this->mysql) { + $pdo = null; + try { + $pdo = new \PDO('mysql:host=localhost;', 'root'); + } catch (\PDOException $e) { + $this->mysql = new NullStorage('MySQL', 'Unable to connect to MySQL on root@localhost'); + } + + if ($pdo) { + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $this->removeMysqlDb($pdo); + $this->createMysqlDb($pdo); + + $this->mysql = new Pdo($pdo); + } + } + + return $this->mysql; + } + + public function getMongo() + { + if (!$this->mongo) { + $skipMongo = $this->getEnvVar('SKIP_MONGO_TESTS'); + if (!$skipMongo && class_exists('MongoClient')) { + $mongo = new \MongoClient('mongodb://localhost:27017', array('connect' => false)); + if ($this->testMongoConnection($mongo)) { + $db = $mongo->oauth2_server_php; + $this->removeMongoDb($db); + $this->createMongoDb($db); + + $this->mongo = new Mongo($db); + } else { + $this->mongo = new NullStorage('Mongo', 'Unable to connect to mongo server on "localhost:27017"'); + } + } else { + $this->mongo = new NullStorage('Mongo', 'Missing mongo php extension. Please install mongo.so'); + } + } + + return $this->mongo; + } + + private function testMongoConnection(\MongoClient $mongo) + { + try { + $mongo->connect(); + } catch (\MongoConnectionException $e) { + return false; + } + + return true; + } + + public function getCouchbase() + { + if (!$this->couchbase) { + if ($this->getEnvVar('SKIP_COUCHBASE_TESTS')) { + $this->couchbase = new NullStorage('Couchbase', 'Skipping Couchbase tests'); + } elseif (!class_exists('Couchbase')) { + $this->couchbase = new NullStorage('Couchbase', 'Missing Couchbase php extension. Please install couchbase.so'); + } else { + // round-about way to make sure couchbase is working + // this is required because it throws a "floating point exception" otherwise + $code = "new \Couchbase(array('localhost:8091'), '', '', 'auth', false);"; + $exec = sprintf('php -r "%s"', $code); + $ret = exec($exec, $test, $var); + if ($ret != 0) { + $couchbase = new \Couchbase(array('localhost:8091'), '', '', 'auth', false); + if ($this->testCouchbaseConnection($couchbase)) { + $this->clearCouchbase($couchbase); + $this->createCouchbaseDB($couchbase); + + $this->couchbase = new CouchbaseDB($couchbase); + } else { + $this->couchbase = new NullStorage('Couchbase', 'Unable to connect to Couchbase server on "localhost:8091"'); + } + } else { + $this->couchbase = new NullStorage('Couchbase', 'Error while trying to connect to Couchbase'); + } + } + } + + return $this->couchbase; + } + + private function testCouchbaseConnection(\Couchbase $couchbase) + { + try { + if (count($couchbase->getServers()) > 0) { + return true; + } + } catch (\CouchbaseException $e) { + return false; + } + + return true; + } + + public function getCassandraStorage() + { + if (!$this->cassandra) { + if (class_exists('phpcassa\ColumnFamily')) { + $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_test', array('127.0.0.1:9160')); + if ($this->testCassandraConnection($cassandra)) { + $this->removeCassandraDb(); + $this->cassandra = new Cassandra($cassandra); + $this->createCassandraDb($this->cassandra); + } else { + $this->cassandra = new NullStorage('Cassandra', 'Unable to connect to cassandra server on "127.0.0.1:9160"'); + } + } else { + $this->cassandra = new NullStorage('Cassandra', 'Missing cassandra library. Please run "composer.phar require thobbs/phpcassa:dev-master"'); + } + } + + return $this->cassandra; + } + + private function testCassandraConnection(\phpcassa\Connection\ConnectionPool $cassandra) + { + try { + new \phpcassa\SystemManager('localhost:9160'); + } catch (\Exception $e) { + return false; + } + + return true; + } + + private function removeCassandraDb() + { + $sys = new \phpcassa\SystemManager('localhost:9160'); + + try { + $sys->drop_keyspace('oauth2_test'); + } catch (\cassandra\InvalidRequestException $e) { + + } + } + + private function createCassandraDb(Cassandra $storage) + { + // create the cassandra keyspace and column family + $sys = new \phpcassa\SystemManager('localhost:9160'); + + $sys->create_keyspace('oauth2_test', array( + "strategy_class" => \phpcassa\Schema\StrategyClass::SIMPLE_STRATEGY, + "strategy_options" => array('replication_factor' => '1') + )); + + $sys->create_column_family('oauth2_test', 'auth'); + $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_test', array('127.0.0.1:9160')); + $cf = new \phpcassa\ColumnFamily($cassandra, 'auth'); + + // populate the data + $storage->setClientDetails("oauth_test_client", "testpass", "http://example.com", 'implicit password'); + $storage->setAccessToken("testtoken", "Some Client", '', time() + 1000); + $storage->setAuthorizationCode("testcode", "Some Client", '', '', time() + 1000); + + $storage->setScope('supportedscope1 supportedscope2 supportedscope3 supportedscope4'); + $storage->setScope('defaultscope1 defaultscope2', null, 'default'); + + $storage->setScope('clientscope1 clientscope2', 'Test Client ID'); + $storage->setScope('clientscope1 clientscope2', 'Test Client ID', 'default'); + + $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Client ID 2'); + $storage->setScope('clientscope1 clientscope2', 'Test Client ID 2', 'default'); + + $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID'); + $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID', 'default'); + + $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Default Scope Client ID 2'); + $storage->setScope('clientscope3', 'Test Default Scope Client ID 2', 'default'); + + $storage->setClientKey('oauth_test_client', $this->getTestPublicKey(), 'test_subject'); + + $cf->insert("oauth_public_keys:ClientID_One", array('__data' => json_encode(array("public_key" => "client_1_public", "private_key" => "client_1_private", "encryption_algorithm" => "RS256")))); + $cf->insert("oauth_public_keys:ClientID_Two", array('__data' => json_encode(array("public_key" => "client_2_public", "private_key" => "client_2_private", "encryption_algorithm" => "RS256")))); + $cf->insert("oauth_public_keys:", array('__data' => json_encode(array("public_key" => $this->getTestPublicKey(), "private_key" => $this->getTestPrivateKey(), "encryption_algorithm" => "RS256")))); + + $cf->insert("oauth_users:testuser", array('__data' =>json_encode(array("password" => "password", "email" => "testuser@test.com", "email_verified" => true)))); + + } + + private function createSqliteDb(\PDO $pdo) + { + $this->runPdoSql($pdo); + } + + private function removeSqliteDb() + { + if (file_exists($this->getSqliteDir())) { + unlink($this->getSqliteDir()); + } + } + + private function createMysqlDb(\PDO $pdo) + { + $pdo->exec('CREATE DATABASE oauth2_server_php'); + $pdo->exec('USE oauth2_server_php'); + $this->runPdoSql($pdo); + } + + private function removeMysqlDb(\PDO $pdo) + { + $pdo->exec('DROP DATABASE IF EXISTS oauth2_server_php'); + } + + private function createPostgresDb() + { + if (!`psql postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='postgres'"`) { + `createuser -s -r postgres`; + } + + `createdb -O postgres oauth2_server_php`; + } + + private function populatePostgresDb(\PDO $pdo) + { + $this->runPdoSql($pdo); + } + + private function removePostgresDb() + { + if (trim(`psql -l | grep oauth2_server_php | wc -l`)) { + `dropdb oauth2_server_php`; + } + } + + public function runPdoSql(\PDO $pdo) + { + $storage = new Pdo($pdo); + foreach (explode(';', $storage->getBuildSql()) as $statement) { + $result = $pdo->exec($statement); + } + + // set up scopes + $sql = 'INSERT INTO oauth_scopes (scope) VALUES (?)'; + foreach (explode(' ', 'supportedscope1 supportedscope2 supportedscope3 supportedscope4 clientscope1 clientscope2 clientscope3') as $supportedScope) { + $pdo->prepare($sql)->execute(array($supportedScope)); + } + + $sql = 'INSERT INTO oauth_scopes (scope, is_default) VALUES (?, ?)'; + foreach (array('defaultscope1', 'defaultscope2') as $defaultScope) { + $pdo->prepare($sql)->execute(array($defaultScope, true)); + } + + // set up clients + $sql = 'INSERT INTO oauth_clients (client_id, client_secret, scope, grant_types) VALUES (?, ?, ?, ?)'; + $pdo->prepare($sql)->execute(array('Test Client ID', 'TestSecret', 'clientscope1 clientscope2', null)); + $pdo->prepare($sql)->execute(array('Test Client ID 2', 'TestSecret', 'clientscope1 clientscope2 clientscope3', null)); + $pdo->prepare($sql)->execute(array('Test Default Scope Client ID', 'TestSecret', 'clientscope1 clientscope2', null)); + $pdo->prepare($sql)->execute(array('oauth_test_client', 'testpass', null, 'implicit password')); + + // set up misc + $sql = 'INSERT INTO oauth_access_tokens (access_token, client_id, expires, user_id) VALUES (?, ?, ?, ?)'; + $pdo->prepare($sql)->execute(array('testtoken', 'Some Client', date('Y-m-d H:i:s', strtotime('+1 hour')), null)); + $pdo->prepare($sql)->execute(array('accesstoken-openid-connect', 'Some Client', date('Y-m-d H:i:s', strtotime('+1 hour')), 'testuser')); + + $sql = 'INSERT INTO oauth_authorization_codes (authorization_code, client_id, expires) VALUES (?, ?, ?)'; + $pdo->prepare($sql)->execute(array('testcode', 'Some Client', date('Y-m-d H:i:s', strtotime('+1 hour')))); + + $sql = 'INSERT INTO oauth_users (username, password, email, email_verified) VALUES (?, ?, ?, ?)'; + $pdo->prepare($sql)->execute(array('testuser', 'password', 'testuser@test.com', true)); + + $sql = 'INSERT INTO oauth_public_keys (client_id, public_key, private_key, encryption_algorithm) VALUES (?, ?, ?, ?)'; + $pdo->prepare($sql)->execute(array('ClientID_One', 'client_1_public', 'client_1_private', 'RS256')); + $pdo->prepare($sql)->execute(array('ClientID_Two', 'client_2_public', 'client_2_private', 'RS256')); + + $sql = 'INSERT INTO oauth_public_keys (client_id, public_key, private_key, encryption_algorithm) VALUES (?, ?, ?, ?)'; + $pdo->prepare($sql)->execute(array(null, $this->getTestPublicKey(), $this->getTestPrivateKey(), 'RS256')); + + $sql = 'INSERT INTO oauth_jwt (client_id, subject, public_key) VALUES (?, ?, ?)'; + $pdo->prepare($sql)->execute(array('oauth_test_client', 'test_subject', $this->getTestPublicKey())); + } + + public function getSqliteDir() + { + return $this->configDir. '/test.sqlite'; + } + + public function getConfigDir() + { + return $this->configDir; + } + + private function createCouchbaseDB(\Couchbase $db) + { + $db->set('oauth_clients-oauth_test_client',json_encode(array( + 'client_id' => "oauth_test_client", + 'client_secret' => "testpass", + 'redirect_uri' => "http://example.com", + 'grant_types' => 'implicit password' + ))); + + $db->set('oauth_access_tokens-testtoken',json_encode(array( + 'access_token' => "testtoken", + 'client_id' => "Some Client" + ))); + + $db->set('oauth_authorization_codes-testcode',json_encode(array( + 'access_token' => "testcode", + 'client_id' => "Some Client" + ))); + + $db->set('oauth_users-testuser',json_encode(array( + 'username' => "testuser", + 'password' => "password" + ))); + + $db->set('oauth_jwt-oauth_test_client',json_encode(array( + 'client_id' => 'oauth_test_client', + 'key' => $this->getTestPublicKey(), + 'subject' => 'test_subject', + ))); + } + + private function clearCouchbase(\Couchbase $cb) + { + $cb->delete('oauth_authorization_codes-new-openid-code'); + $cb->delete('oauth_access_tokens-newtoken'); + $cb->delete('oauth_authorization_codes-newcode'); + $cb->delete('oauth_refresh_tokens-refreshtoken'); + } + + private function createMongoDb(\MongoDB $db) + { + $db->oauth_clients->insert(array( + 'client_id' => "oauth_test_client", + 'client_secret' => "testpass", + 'redirect_uri' => "http://example.com", + 'grant_types' => 'implicit password' + )); + + $db->oauth_access_tokens->insert(array( + 'access_token' => "testtoken", + 'client_id' => "Some Client" + )); + + $db->oauth_authorization_codes->insert(array( + 'authorization_code' => "testcode", + 'client_id' => "Some Client" + )); + + $db->oauth_users->insert(array( + 'username' => "testuser", + 'password' => "password" + )); + + $db->oauth_jwt->insert(array( + 'client_id' => 'oauth_test_client', + 'key' => $this->getTestPublicKey(), + 'subject' => 'test_subject', + )); + } + + private function createRedisDb(Redis $storage) + { + $storage->setClientDetails("oauth_test_client", "testpass", "http://example.com", 'implicit password'); + $storage->setAccessToken("testtoken", "Some Client", '', time() + 1000); + $storage->setAuthorizationCode("testcode", "Some Client", '', '', time() + 1000); + $storage->setUser("testuser", "password"); + + $storage->setScope('supportedscope1 supportedscope2 supportedscope3 supportedscope4'); + $storage->setScope('defaultscope1 defaultscope2', null, 'default'); + + $storage->setScope('clientscope1 clientscope2', 'Test Client ID'); + $storage->setScope('clientscope1 clientscope2', 'Test Client ID', 'default'); + + $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Client ID 2'); + $storage->setScope('clientscope1 clientscope2', 'Test Client ID 2', 'default'); + + $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID'); + $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID', 'default'); + + $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Default Scope Client ID 2'); + $storage->setScope('clientscope3', 'Test Default Scope Client ID 2', 'default'); + + $storage->setClientKey('oauth_test_client', $this->getTestPublicKey(), 'test_subject'); + } + + public function removeMongoDb(\MongoDB $db) + { + $db->drop(); + } + + public function getTestPublicKey() + { + return file_get_contents(__DIR__.'/../../../config/keys/id_rsa.pub'); + } + + private function getTestPrivateKey() + { + return file_get_contents(__DIR__.'/../../../config/keys/id_rsa'); + } + + public function getDynamoDbStorage() + { + if (!$this->dynamodb) { + // only run once per travis build + if (true == $this->getEnvVar('TRAVIS')) { + if (self::DYNAMODB_PHP_VERSION != $this->getEnvVar('TRAVIS_PHP_VERSION')) { + $this->dynamodb = new NullStorage('DynamoDb', 'Skipping for travis.ci - only run once per build'); + + return; + } + } + if (class_exists('\Aws\DynamoDb\DynamoDbClient')) { + if ($client = $this->getDynamoDbClient()) { + // travis runs a unique set of tables per build, to avoid conflict + $prefix = ''; + if ($build_id = $this->getEnvVar('TRAVIS_JOB_NUMBER')) { + $prefix = sprintf('build_%s_', $build_id); + } else { + if (!$this->deleteDynamoDb($client, $prefix, true)) { + return $this->dynamodb = new NullStorage('DynamoDb', 'Timed out while waiting for DynamoDB deletion (30 seconds)'); + } + } + $this->createDynamoDb($client, $prefix); + $this->populateDynamoDb($client, $prefix); + $config = array( + 'client_table' => $prefix.'oauth_clients', + 'access_token_table' => $prefix.'oauth_access_tokens', + 'refresh_token_table' => $prefix.'oauth_refresh_tokens', + 'code_table' => $prefix.'oauth_authorization_codes', + 'user_table' => $prefix.'oauth_users', + 'jwt_table' => $prefix.'oauth_jwt', + 'scope_table' => $prefix.'oauth_scopes', + 'public_key_table' => $prefix.'oauth_public_keys', + ); + $this->dynamodb = new DynamoDB($client, $config); + } elseif (!$this->dynamodb) { + $this->dynamodb = new NullStorage('DynamoDb', 'unable to connect to DynamoDB'); + } + } else { + $this->dynamodb = new NullStorage('DynamoDb', 'Missing DynamoDB library. Please run "composer.phar require aws/aws-sdk-php:dev-master'); + } + } + + return $this->dynamodb; + } + + private function getDynamoDbClient() + { + $config = array(); + // check for environment variables + if (($key = $this->getEnvVar('AWS_ACCESS_KEY_ID')) && ($secret = $this->getEnvVar('AWS_SECRET_KEY'))) { + $config['key'] = $key; + $config['secret'] = $secret; + } else { + // fall back on ~/.aws/credentials file + // @see http://docs.aws.amazon.com/aws-sdk-php/guide/latest/credentials.html#credential-profiles + if (!file_exists($this->getEnvVar('HOME') . '/.aws/credentials')) { + $this->dynamodb = new NullStorage('DynamoDb', 'No aws credentials file found, and no AWS_ACCESS_KEY_ID or AWS_SECRET_KEY environment variable set'); + + return; + } + + // set profile in AWS_PROFILE environment variable, defaults to "default" + $config['profile'] = $this->getEnvVar('AWS_PROFILE', 'default'); + } + + // set region in AWS_REGION environment variable, defaults to "us-east-1" + $config['region'] = $this->getEnvVar('AWS_REGION', \Aws\Common\Enum\Region::US_EAST_1); + + return \Aws\DynamoDb\DynamoDbClient::factory($config); + } + + private function deleteDynamoDb(\Aws\DynamoDb\DynamoDbClient $client, $prefix = null, $waitForDeletion = false) + { + $tablesList = explode(' ', 'oauth_access_tokens oauth_authorization_codes oauth_clients oauth_jwt oauth_public_keys oauth_refresh_tokens oauth_scopes oauth_users'); + $nbTables = count($tablesList); + + // Delete all table. + foreach ($tablesList as $key => $table) { + try { + $client->deleteTable(array('TableName' => $prefix.$table)); + } catch (\Aws\DynamoDb\Exception\DynamoDbException $e) { + // Table does not exist : nothing to do + } + } + + // Wait for deleting + if ($waitForDeletion) { + $retries = 5; + $nbTableDeleted = 0; + while ($nbTableDeleted != $nbTables) { + $nbTableDeleted = 0; + foreach ($tablesList as $key => $table) { + try { + $result = $client->describeTable(array('TableName' => $prefix.$table)); + } catch (\Aws\DynamoDb\Exception\DynamoDbException $e) { + // Table does not exist : nothing to do + $nbTableDeleted++; + } + } + if ($nbTableDeleted != $nbTables) { + if ($retries < 0) { + // we are tired of waiting + return false; + } + sleep(5); + echo "Sleeping 5 seconds for DynamoDB ($retries more retries)...\n"; + $retries--; + } + } + } + + return true; + } + + private function createDynamoDb(\Aws\DynamoDb\DynamoDbClient $client, $prefix = null) + { + $tablesList = explode(' ', 'oauth_access_tokens oauth_authorization_codes oauth_clients oauth_jwt oauth_public_keys oauth_refresh_tokens oauth_scopes oauth_users'); + $nbTables = count($tablesList); + $client->createTable(array( + 'TableName' => $prefix.'oauth_access_tokens', + 'AttributeDefinitions' => array( + array('AttributeName' => 'access_token','AttributeType' => 'S') + ), + 'KeySchema' => array(array('AttributeName' => 'access_token','KeyType' => 'HASH')), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + + $client->createTable(array( + 'TableName' => $prefix.'oauth_authorization_codes', + 'AttributeDefinitions' => array( + array('AttributeName' => 'authorization_code','AttributeType' => 'S') + ), + 'KeySchema' => array(array('AttributeName' => 'authorization_code','KeyType' => 'HASH')), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + + $client->createTable(array( + 'TableName' => $prefix.'oauth_clients', + 'AttributeDefinitions' => array( + array('AttributeName' => 'client_id','AttributeType' => 'S') + ), + 'KeySchema' => array(array('AttributeName' => 'client_id','KeyType' => 'HASH')), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + + $client->createTable(array( + 'TableName' => $prefix.'oauth_jwt', + 'AttributeDefinitions' => array( + array('AttributeName' => 'client_id','AttributeType' => 'S'), + array('AttributeName' => 'subject','AttributeType' => 'S') + ), + 'KeySchema' => array( + array('AttributeName' => 'client_id','KeyType' => 'HASH'), + array('AttributeName' => 'subject','KeyType' => 'RANGE') + ), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + + $client->createTable(array( + 'TableName' => $prefix.'oauth_public_keys', + 'AttributeDefinitions' => array( + array('AttributeName' => 'client_id','AttributeType' => 'S') + ), + 'KeySchema' => array(array('AttributeName' => 'client_id','KeyType' => 'HASH')), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + + $client->createTable(array( + 'TableName' => $prefix.'oauth_refresh_tokens', + 'AttributeDefinitions' => array( + array('AttributeName' => 'refresh_token','AttributeType' => 'S') + ), + 'KeySchema' => array(array('AttributeName' => 'refresh_token','KeyType' => 'HASH')), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + + $client->createTable(array( + 'TableName' => $prefix.'oauth_scopes', + 'AttributeDefinitions' => array( + array('AttributeName' => 'scope','AttributeType' => 'S'), + array('AttributeName' => 'is_default','AttributeType' => 'S') + ), + 'KeySchema' => array(array('AttributeName' => 'scope','KeyType' => 'HASH')), + 'GlobalSecondaryIndexes' => array( + array( + 'IndexName' => 'is_default-index', + 'KeySchema' => array(array('AttributeName' => 'is_default', 'KeyType' => 'HASH')), + 'Projection' => array('ProjectionType' => 'ALL'), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + ), + ), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + + $client->createTable(array( + 'TableName' => $prefix.'oauth_users', + 'AttributeDefinitions' => array(array('AttributeName' => 'username','AttributeType' => 'S')), + 'KeySchema' => array(array('AttributeName' => 'username','KeyType' => 'HASH')), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + + // Wait for creation + $nbTableCreated = 0; + while ($nbTableCreated != $nbTables) { + $nbTableCreated = 0; + foreach ($tablesList as $key => $table) { + try { + $result = $client->describeTable(array('TableName' => $prefix.$table)); + if ($result['Table']['TableStatus'] == 'ACTIVE') { + $nbTableCreated++; + } + } catch (\Aws\DynamoDb\Exception\DynamoDbException $e) { + // Table does not exist : nothing to do + $nbTableCreated++; + } + } + if ($nbTableCreated != $nbTables) { + sleep(1); + } + } + } + + private function populateDynamoDb($client, $prefix = null) + { + // set up scopes + foreach (explode(' ', 'supportedscope1 supportedscope2 supportedscope3 supportedscope4 clientscope1 clientscope2 clientscope3') as $supportedScope) { + $client->putItem(array( + 'TableName' => $prefix.'oauth_scopes', + 'Item' => array('scope' => array('S' => $supportedScope)) + )); + } + + foreach (array('defaultscope1', 'defaultscope2') as $defaultScope) { + $client->putItem(array( + 'TableName' => $prefix.'oauth_scopes', + 'Item' => array('scope' => array('S' => $defaultScope), 'is_default' => array('S' => "true")) + )); + } + + $client->putItem(array( + 'TableName' => $prefix.'oauth_clients', + 'Item' => array( + 'client_id' => array('S' => 'Test Client ID'), + 'client_secret' => array('S' => 'TestSecret'), + 'scope' => array('S' => 'clientscope1 clientscope2') + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_clients', + 'Item' => array( + 'client_id' => array('S' => 'Test Client ID 2'), + 'client_secret' => array('S' => 'TestSecret'), + 'scope' => array('S' => 'clientscope1 clientscope2 clientscope3') + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_clients', + 'Item' => array( + 'client_id' => array('S' => 'Test Default Scope Client ID'), + 'client_secret' => array('S' => 'TestSecret'), + 'scope' => array('S' => 'clientscope1 clientscope2') + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_clients', + 'Item' => array( + 'client_id' => array('S' => 'oauth_test_client'), + 'client_secret' => array('S' => 'testpass'), + 'grant_types' => array('S' => 'implicit password') + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_access_tokens', + 'Item' => array( + 'access_token' => array('S' => 'testtoken'), + 'client_id' => array('S' => 'Some Client'), + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_access_tokens', + 'Item' => array( + 'access_token' => array('S' => 'accesstoken-openid-connect'), + 'client_id' => array('S' => 'Some Client'), + 'user_id' => array('S' => 'testuser'), + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_authorization_codes', + 'Item' => array( + 'authorization_code' => array('S' => 'testcode'), + 'client_id' => array('S' => 'Some Client'), + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_users', + 'Item' => array( + 'username' => array('S' => 'testuser'), + 'password' => array('S' => 'password'), + 'email' => array('S' => 'testuser@test.com'), + 'email_verified' => array('S' => 'true'), + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_public_keys', + 'Item' => array( + 'client_id' => array('S' => 'ClientID_One'), + 'public_key' => array('S' => 'client_1_public'), + 'private_key' => array('S' => 'client_1_private'), + 'encryption_algorithm' => array('S' => 'RS256'), + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_public_keys', + 'Item' => array( + 'client_id' => array('S' => 'ClientID_Two'), + 'public_key' => array('S' => 'client_2_public'), + 'private_key' => array('S' => 'client_2_private'), + 'encryption_algorithm' => array('S' => 'RS256'), + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_public_keys', + 'Item' => array( + 'client_id' => array('S' => '0'), + 'public_key' => array('S' => $this->getTestPublicKey()), + 'private_key' => array('S' => $this->getTestPrivateKey()), + 'encryption_algorithm' => array('S' => 'RS256'), + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_jwt', + 'Item' => array( + 'client_id' => array('S' => 'oauth_test_client'), + 'subject' => array('S' => 'test_subject'), + 'public_key' => array('S' => $this->getTestPublicKey()), + ) + )); + } + + public function cleanupTravisDynamoDb($prefix = null) + { + if (is_null($prefix)) { + // skip this when not applicable + if (!$this->getEnvVar('TRAVIS') || self::DYNAMODB_PHP_VERSION != $this->getEnvVar('TRAVIS_PHP_VERSION')) { + return; + } + + $prefix = sprintf('build_%s_', $this->getEnvVar('TRAVIS_JOB_NUMBER')); + } + + $client = $this->getDynamoDbClient(); + $this->deleteDynamoDb($client, $prefix); + } + + private function getEnvVar($var, $default = null) + { + return isset($_SERVER[$var]) ? $_SERVER[$var] : (getenv($var) ?: $default); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/NullStorage.php b/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/NullStorage.php new file mode 100644 index 0000000..6caa620 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/NullStorage.php @@ -0,0 +1,32 @@ +name = $name; + $this->description = $description; + } + + public function __toString() + { + return $this->name; + } + + public function getMessage() + { + if ($this->description) { + return $this->description; + } + + return $this->name; + } +} diff --git a/vendor/codeguy/upload/LICENSE b/vendor/codeguy/upload/LICENSE new file mode 100644 index 0000000..ec361cb --- /dev/null +++ b/vendor/codeguy/upload/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 Josh Lockhart + +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: + +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/vendor/codeguy/upload/README.md b/vendor/codeguy/upload/README.md new file mode 100644 index 0000000..5ec586f --- /dev/null +++ b/vendor/codeguy/upload/README.md @@ -0,0 +1,75 @@ +# Upload + +## Usage + +This component simplifies file validation and uploading. Assume a file is uploaded with this HTML form: + +
+ + +
+ +When the HTML form is submitted, the server-side PHP code can validate and upload the file like this: + + setName($new_filename); + + // Validate file upload + // MimeType List => http://www.webmaster-toolkit.com/mime-types.shtml + $file->addValidations(array( + // Ensure file is of type "image/png" + new \Upload\Validation\Mimetype('image/png'), + + // Ensure file is no larger than 5M (use "B", "K", M", or "G") + new \Upload\Validation\Size('5M') + )); + + // Access data about the file that has been uploaded + $data = array( + 'name' => $file->getNameWithExtension(), + 'extension' => $file->getExtension(), + 'mime' => $file->getMimetype(), + 'size' => $file->getSize(), + 'md5' => $file->getMd5(), + 'dimensions' => $file->getDimensions() + ); + + // Try to upload file + try { + // Success! + $file->upload(); + } catch (\Exception $e) { + // Fail! + $errors = $file->getErrors(); + } + +## How to Install + +Install composer in your project: + + curl -s https://getcomposer.org/installer | php + +Create a composer.json file in your project root: + + { + "require": { + "codeguy/upload": "*" + } + } + +Install via composer: + + php composer.phar install + +## Author + +[Josh Lockhart](https://github.com/codeguy) + +## License + +MIT Public License diff --git a/vendor/codeguy/upload/composer.json b/vendor/codeguy/upload/composer.json new file mode 100644 index 0000000..f0b7421 --- /dev/null +++ b/vendor/codeguy/upload/composer.json @@ -0,0 +1,25 @@ +{ + "name": "codeguy/upload", + "type": "library", + "description": "Handle file uploads with extensible validation and storage strategies", + "keywords": ["upload", "validation", "file"], + "homepage": "http://github.com/codeguy/Upload", + "license": "MIT", + "authors": [ + { + "name": "Josh Lockhart", + "email": "info@joshlockhart.com", + "homepage": "https://github.com/codeguy/" + } + ], + "require": { + "php": ">=5.3.0", + "ext-fileinfo": "*" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, + "autoload": { + "psr-0": { "Upload": "src" } + } +} diff --git a/vendor/codeguy/upload/phpunit.xml b/vendor/codeguy/upload/phpunit.xml new file mode 100644 index 0000000..b2d1ef0 --- /dev/null +++ b/vendor/codeguy/upload/phpunit.xml @@ -0,0 +1,21 @@ + + + + ./tests/ + + + + + ./src/Upload/ + + + diff --git a/vendor/codeguy/upload/src/Upload/Autoloader.php b/vendor/codeguy/upload/src/Upload/Autoloader.php new file mode 100644 index 0000000..940823e --- /dev/null +++ b/vendor/codeguy/upload/src/Upload/Autoloader.php @@ -0,0 +1,77 @@ + + * @copyright 2012 Josh Lockhart + * @link http://www.joshlockhart.com + * @version 1.0.0 + * + * MIT LICENSE + * + * 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: + * + * 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. + */ +namespace Upload; + +/** + * Autoloader + * + * This class provides a default PSR-0 autoloader if not using Composer. + * + * @author Josh Lockhart + * @since 1.0.0 + * @package Upload + */ +class Autoloader +{ + /** + * The project's base directory + * @var string + */ + static protected $base; + + /** + * Register autoloader + */ + static public function register() + { + self::$base = dirname(__FILE__) . '/../'; + spl_autoload_register(array(new self, 'autoload')); + } + + /** + * Autoload classname + * @param string $className The class to load + */ + static public function autoload($className) + { + $className = ltrim($className, '\\'); + $fileName = ''; + $namespace = ''; + if ($lastNsPos = strripos($className, '\\')) { + $namespace = substr($className, 0, $lastNsPos); + $className = substr($className, $lastNsPos + 1); + $fileName = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR; + } + $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php'; + + require self::$base . $fileName; + } +} diff --git a/vendor/codeguy/upload/src/Upload/Exception/UploadException.php b/vendor/codeguy/upload/src/Upload/Exception/UploadException.php new file mode 100644 index 0000000..86c927a --- /dev/null +++ b/vendor/codeguy/upload/src/Upload/Exception/UploadException.php @@ -0,0 +1,7 @@ + + * @copyright 2012 Josh Lockhart + * @link http://www.joshlockhart.com + * @version 1.0.0 + * + * MIT LICENSE + * + * 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: + * + * 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. + */ +namespace Upload; + +/** + * File + * + * This class provides the implementation for an uploaded file. It exposes + * common attributes for the uploaded file (e.g. name, extension, media type) + * and allows you to attach validations to the file that must pass for the + * upload to succeed. + * + * @author Josh Lockhart + * @since 1.0.0 + * @package Upload + */ +class File extends \SplFileInfo +{ + /******************************************************************************** + * Static Properties + *******************************************************************************/ + + /** + * Upload error code messages + * @var array + */ + protected static $errorCodeMessages = array( + 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', + 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', + 3 => 'The uploaded file was only partially uploaded', + 4 => 'No file was uploaded', + 6 => 'Missing a temporary folder', + 7 => 'Failed to write file to disk', + 8 => 'A PHP extension stopped the file upload' + ); + + /** + * Lookup hash to convert file units to bytes + * @var array + */ + protected static $units = array( + 'b' => 1, + 'k' => 1024, + 'm' => 1048576, + 'g' => 1073741824 + ); + + /******************************************************************************** + * Instance Properties + *******************************************************************************/ + + /** + * Storage delegate + * @var \Upload\Storage\Base + */ + protected $storage; + + /** + * Validations + * @var array[\Upload\Validation\Base] + */ + protected $validations; + + /** + * Validation errors + * @var array + */ + protected $errors; + + /** + * Original file name provided by client (for internal use only) + * @var string + */ + protected $originalName; + + /** + * File name (without extension) + * @var string + */ + protected $name; + + /** + * File extension (without leading dot) + * @var string + */ + protected $extension; + + /** + * File mimetype (e.g. "image/png") + * @var string + */ + protected $mimetype; + + /** + * Upload error code (for internal use only) + * @var int + * @link http://www.php.net/manual/en/features.file-upload.errors.php + */ + protected $errorCode; + + /** + * Constructor + * @param string $key The file's key in $_FILES superglobal + * @param \Upload\Storage\Base $storage The method with which to store file + * @throws \Upload\Exception\UploadException If file uploads are disabled in the php.ini file + * @throws \InvalidArgumentException If $_FILES key does not exist + */ + public function __construct($key, \Upload\Storage\Base $storage) + { + if (!isset($_FILES[$key])) { + throw new \InvalidArgumentException("Cannot find uploaded file identified by key: $key"); + } + $this->storage = $storage; + $this->validations = array(); + $this->errors = array(); + $this->originalName = $_FILES[$key]['name']; + $this->errorCode = $_FILES[$key]['error']; + parent::__construct($_FILES[$key]['tmp_name']); + } + + /** + * Get name + * @return string + */ + public function getName() + { + if (!isset($this->name)) { + $this->name = pathinfo($this->originalName, PATHINFO_FILENAME); + } + + return $this->name; + } + + /** + * Set name (without extension) + * @param string $name + * @return \Upload\File Self + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get file name with extension + * @return string + */ + public function getNameWithExtension() + { + return sprintf('%s.%s', $this->getName(), $this->getExtension()); + } + + /** + * Get file extension (without leading dot) + * @return string + */ + public function getExtension() + { + if (!isset($this->extension)) { + $this->extension = strtolower(pathinfo($this->originalName, PATHINFO_EXTENSION)); + } + + return $this->extension; + } + + /** + * Get mimetype + * @return string + */ + public function getMimetype() + { + if (!isset($this->mimeType)) { + $finfo = new \finfo(FILEINFO_MIME); + $mimetype = $finfo->file($this->getPathname()); + $mimetypeParts = preg_split('/\s*[;,]\s*/', $mimetype); + $this->mimetype = strtolower($mimetypeParts[0]); + unset($finfo); + } + + return $this->mimetype; + } + + /** + * Get md5 + * @return string + */ + public function getMd5() + { + return md5_file($this->getPathname()); + } + + /** + * Get image dimensions + * @return array formatted array of dimensions + */ + public function getDimensions() + { + list($width, $height) = getimagesize($this->getPathname()); + return array( + 'width' => $width, + 'height' => $height + ); + } + + /******************************************************************************** + * Validate + *******************************************************************************/ + + /** + * Add file validations + * @param \Upload\Validation\Base|array[\Upload\Validation\Base] $validations + */ + public function addValidations($validations) + { + if (!is_array($validations)) { + $validations = array($validations); + } + foreach ($validations as $validation) { + if ($validation instanceof \Upload\Validation\Base) { + $this->validations[] = $validation; + } + } + } + + /** + * Get file validations + * @return array[\Upload\Validation\Base] + */ + public function getValidations() + { + return $this->validations; + } + + /** + * Validate file + * @return bool True if valid, false if invalid + */ + public function validate() + { + // Validate is uploaded OK + if ($this->isOk() === false) { + $this->errors[] = self::$errorCodeMessages[$this->errorCode]; + } + + // Validate is uploaded file + if ($this->isUploadedFile() === false) { + $this->errors[] = 'The uploaded file was not sent with a POST request'; + } + + // User validations + foreach ($this->validations as $validation) { + if ($validation->validate($this) === false) { + $this->errors[] = $validation->getMessage(); + } + } + + return empty($this->errors); + } + + /** + * Get file validation errors + * @return array[String] + */ + public function getErrors() + { + return $this->errors; + } + + /** + * Add file validation error + * @param string + * @return \Upload\File Self + */ + public function addError($error) + { + $this->errors[] = $error; + + return $this; + } + + /******************************************************************************** + * Upload + *******************************************************************************/ + + /** + * Upload file (delegated to storage object) + * @param string $newName Give the file it a new name + * @return bool + * @throws \Upload\Exception\UploadException If file does not validate + */ + public function upload($newName = null) + { + if ($this->validate() === false) { + throw new \Upload\Exception\UploadException('File validation failed'); + } + + // Update the name, leaving out the extension + if (is_string($newName)) { + $this->name = pathinfo($newName, PATHINFO_FILENAME); + } + + return $this->storage->upload($this, $newName); + } + + /******************************************************************************** + * Helpers + *******************************************************************************/ + + /** + * Is this file uploaded with a POST request? + * + * This is a separate method so that it can be stubbed in unit tests to avoid + * the hard dependency on the `is_uploaded_file` function. + * + * @return bool + */ + public function isUploadedFile() + { + return is_uploaded_file($this->getPathname()); + } + + /** + * Is this file OK? + * + * This method inspects the upload error code to see if the upload was + * successful or if it failed for a variety of reasons. + * + * @link http://www.php.net/manual/en/features.file-upload.errors.php + * @return bool + */ + public function isOk() + { + return ($this->errorCode === UPLOAD_ERR_OK); + } + + /** + * Convert human readable file size (e.g. "10K" or "3M") into bytes + * @param string $input + * @return int + */ + public static function humanReadableToBytes($input) + { + $number = (int)$input; + $unit = strtolower(substr($input, -1)); + if (isset(self::$units[$unit])) { + $number = $number * self::$units[$unit]; + } + + return $number; + } +} diff --git a/vendor/codeguy/upload/src/Upload/Storage/Base.php b/vendor/codeguy/upload/src/Upload/Storage/Base.php new file mode 100644 index 0000000..0e50dfc --- /dev/null +++ b/vendor/codeguy/upload/src/Upload/Storage/Base.php @@ -0,0 +1,45 @@ + + * @copyright 2012 Josh Lockhart + * @link http://www.joshlockhart.com + * @version 1.0.0 + * + * MIT LICENSE + * + * 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: + * + * 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. + */ +namespace Upload\Storage; + +/** + * Upload Storage Base + * + * This class defines the interface that must be implemented by each + * concrete Upload storage subclass. + * + * @author Josh Lockhart + * @since 1.0.0 + */ +abstract class Base +{ + abstract public function upload(\Upload\File $file, $newName = null); +} diff --git a/vendor/codeguy/upload/src/Upload/Storage/FileSystem.php b/vendor/codeguy/upload/src/Upload/Storage/FileSystem.php new file mode 100644 index 0000000..9c96b9b --- /dev/null +++ b/vendor/codeguy/upload/src/Upload/Storage/FileSystem.php @@ -0,0 +1,114 @@ + + * @copyright 2012 Josh Lockhart + * @link http://www.joshlockhart.com + * @version 1.0.0 + * + * MIT LICENSE + * + * 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: + * + * 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. + */ +namespace Upload\Storage; + +/** + * FileSystem Storage + * + * This class uploads files to a designated directory on the filesystem. + * + * @author Josh Lockhart + * @since 1.0.0 + * @package Upload + */ +class FileSystem extends \Upload\Storage\Base +{ + /** + * Upload directory + * @var string + */ + protected $directory; + + /** + * Overwrite existing files? + * @var bool + */ + protected $overwrite; + + /** + * Constructor + * @param string $directory Relative or absolute path to upload directory + * @param bool $overwrite Should this overwrite existing files? + * @throws \InvalidArgumentException If directory does not exist + * @throws \InvalidArgumentException If directory is not writable + */ + public function __construct($directory, $overwrite = false) + { + if (!is_dir($directory)) { + throw new \InvalidArgumentException('Directory does not exist'); + } + if (!is_writable($directory)) { + throw new \InvalidArgumentException('Directory is not writable'); + } + $this->directory = rtrim($directory, '/') . DIRECTORY_SEPARATOR; + $this->overwrite = $overwrite; + } + + /** + * Upload + * @param \Upload\File $file The file object to upload + * @param string $newName Give the file it a new name + * @return bool + * @throws \RuntimeException If overwrite is false and file already exists + */ + public function upload(\Upload\File $file, $newName = null) + { + if (is_string($newName)) { + $fileName = strpos($newName, '.') ? $newName : $newName.'.'.$file->getExtension(); + + } else { + $fileName = $file->getNameWithExtension(); + } + + $newFile = $this->directory . $fileName; + if ($this->overwrite === false && file_exists($newFile)) { + $file->addError('File already exists'); + throw new \Upload\Exception\UploadException('File already exists'); + } + + return $this->moveUploadedFile($file->getPathname(), $newFile); + } + + /** + * Move uploaded file + * + * This method allows us to stub this method in unit tests to avoid + * hard dependency on the `move_uploaded_file` function. + * + * @param string $source The source file + * @param string $destination The destination file + * @return bool + */ + protected function moveUploadedFile($source, $destination) + { + return move_uploaded_file($source, $destination); + } +} diff --git a/vendor/codeguy/upload/src/Upload/Validation/Base.php b/vendor/codeguy/upload/src/Upload/Validation/Base.php new file mode 100644 index 0000000..17ef794 --- /dev/null +++ b/vendor/codeguy/upload/src/Upload/Validation/Base.php @@ -0,0 +1,75 @@ + + * @copyright 2012 Josh Lockhart + * @link http://www.joshlockhart.com + * @version 1.0.0 + * + * MIT LICENSE + * + * 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: + * + * 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. + */ +namespace Upload\Validation; + +/** + * Upload Validation Base + * + * This class provides the common implementation and abstract interface + * for all concrete Upload validation subclasses. + * + * @author Josh Lockhart + * @since 1.0.0 + * @package Upload + */ +abstract class Base +{ + /** + * The error message for this validation + * @var string + */ + protected $message; + + /** + * Set error message + * @param string $message + */ + public function setMessage($message) + { + $this->message = $message; + } + + /** + * Get error message + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * Validate file + * @param \Upload\File $file + * @return bool True if file is valid, false if file is not valid + */ + abstract public function validate(\Upload\File $file); +} diff --git a/vendor/codeguy/upload/src/Upload/Validation/Extension.php b/vendor/codeguy/upload/src/Upload/Validation/Extension.php new file mode 100644 index 0000000..1a9d8fb --- /dev/null +++ b/vendor/codeguy/upload/src/Upload/Validation/Extension.php @@ -0,0 +1,95 @@ + + * @copyright 2012 Josh Lockhart + * @link http://www.joshlockhart.com + * @version 1.0.0 + * + * MIT LICENSE + * + * 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: + * + * 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. + */ +namespace Upload\Validation; + +/** + * Validate File Extension + * + * This class validates an uploads file extension. It takes file extension with out dot + * or array of extensions. For example: 'png' or array('jpg', 'png', 'gif'). + * + * WARING! Validation only by file extension not very secure. + * + * @author Alex Kucherenko + * @package Upload + */ +class Extension extends \Upload\Validation\Base +{ + /** + * Array of cceptable file extensions without leading dots + * @var array + */ + protected $allowedExtensions; + + /** + * Error message + * @var string + */ + protected $message = 'Invalid file extension. Must be one of: %s'; + + /** + * Constructor + * + * @param string|array $allowedExtensions Allowed file extensions + * @example new \Upload\Validation\Extension(array('png','jpg','gif')) + * @example new \Upload\Validation\Extension('png') + */ + public function __construct($allowedExtensions) + { + if (is_string($allowedExtensions)) { + $allowedExtensions = array($allowedExtensions); + } + + array_filter($allowedExtensions, function ($val) { + return strtolower($val); + }); + + $this->allowedExtensions = $allowedExtensions; + } + + /** + * Validate + * @param \Upload\File $file + * @return bool + */ + public function validate(\Upload\File $file) + { + $fileExtension = strtolower($file->getExtension()); + $isValid = true; + + if (!in_array($fileExtension, $this->allowedExtensions)) { + $this->setMessage(sprintf($this->message, implode(', ', $this->allowedExtensions))); + $isValid = false; + } + + return $isValid; + } +} diff --git a/vendor/codeguy/upload/src/Upload/Validation/Mimetype.php b/vendor/codeguy/upload/src/Upload/Validation/Mimetype.php new file mode 100644 index 0000000..98b6c31 --- /dev/null +++ b/vendor/codeguy/upload/src/Upload/Validation/Mimetype.php @@ -0,0 +1,77 @@ + + * @copyright 2012 Josh Lockhart + * @link http://www.joshlockhart.com + * @version 1.0.0 + * + * MIT LICENSE + * + * 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: + * + * 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. + */ +namespace Upload\Validation; + +/** + * Validate Upload Media Type + * + * This class validates an upload's media type (e.g. "image/png"). + * + * @author Josh Lockhart + * @since 1.0.0 + * @package Upload + */ +class Mimetype extends \Upload\Validation\Base +{ + /** + * Valid media types + * @var array + */ + protected $mimetypes; + + /** + * Error message + * @var string + */ + protected $message = 'Invalid mimetype'; + + /** + * Constructor + * @param array $mimetypes Array of valid mimetypes + */ + public function __construct($mimetypes) + { + if (!is_array($mimetypes)) { + $mimetypes = array($mimetypes); + } + $this->mimetypes = $mimetypes; + } + + /** + * Validate + * @param \Upload\File $file + * @return bool + */ + public function validate(\Upload\File $file) + { + return in_array($file->getMimetype(), $this->mimetypes); + } +} diff --git a/vendor/codeguy/upload/src/Upload/Validation/Size.php b/vendor/codeguy/upload/src/Upload/Validation/Size.php new file mode 100644 index 0000000..6e4ccbe --- /dev/null +++ b/vendor/codeguy/upload/src/Upload/Validation/Size.php @@ -0,0 +1,104 @@ + + * @copyright 2012 Josh Lockhart + * @link http://www.joshlockhart.com + * @version 1.0.0 + * + * MIT LICENSE + * + * 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: + * + * 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. + */ +namespace Upload\Validation; + +/** + * Validate Upload File Size + * + * This class validates an uploads file size using maximum and (optionally) + * minimum file size bounds (inclusive). Specify acceptable file sizes + * as an integer (in bytes) or as a human-readable string (e.g. "5MB"). + * + * @author Josh Lockhart + * @since 1.0.0 + * @package Upload + */ +class Size extends \Upload\Validation\Base +{ + /** + * Minimum acceptable file size (bytes) + * @var int + */ + protected $minSize; + + /** + * Maximum acceptable file size (bytes) + * @var int + */ + protected $maxSize; + + /** + * Error message + * @var string + */ + protected $message = 'Invalid file size'; + + /** + * Constructor + * @param int $maxSize Maximum acceptable file size in bytes (inclusive) + * @param int $minSize Minimum acceptable file size in bytes (inclusive) + */ + public function __construct($maxSize, $minSize = 0) + { + if (is_string($maxSize)) { + $maxSize = \Upload\File::humanReadableToBytes($maxSize); + } + $this->maxSize = $maxSize; + + if (is_string($minSize)) { + $minSize = \Upload\File::humanReadableToBytes($minSize); + } + $this->minSize = $minSize; + } + + /** + * Validate + * @param \Upload\File $file + * @return bool + */ + public function validate(\Upload\File $file) + { + $fileSize = $file->getSize(); + $isValid = true; + + if ($fileSize < $this->minSize) { + $this->setMessage('File size is too small'); + $isValid = false; + } + + if ($fileSize > $this->maxSize) { + $this->setMessage('File size is too large'); + $isValid = false; + } + + return $isValid; + } +} diff --git a/vendor/codeguy/upload/tests/FileTest.php b/vendor/codeguy/upload/tests/FileTest.php new file mode 100644 index 0000000..3394cc8 --- /dev/null +++ b/vendor/codeguy/upload/tests/FileTest.php @@ -0,0 +1,176 @@ +assetsDirectory = dirname(__FILE__) . '/assets'; + $_FILES['foo'] = array( + 'name' => 'foo.txt', + 'tmp_name' => $this->assetsDirectory . '/foo.txt', + 'error' => UPLOAD_ERR_OK + ); + } + + public function getNewFile() + { + if (is_null($this->storage)) { + // Prepare storage + $this->storage = $this->getMock( + '\Upload\Storage\FileSystem', + array('upload'), + array($this->assetsDirectory) + ); + $this->storage->expects($this->any()) + ->method('upload') + ->will($this->returnValue(true)); + } + + // Prepare file + $file = $this->getMock( + '\Upload\File', + array('isUploadedFile'), + array('foo', $this->storage) + ); + $file->expects($this->any()) + ->method('isUploadedFile') + ->will($this->returnValue(true)); + + return $file; + } + + /******************************************************************************** + * Tests + *******************************************************************************/ + + /** + * @expectedException \InvalidArgumentException + */ + public function testConstructionWithInvalidKey() + { + $file = new \Upload\File('bar', new \Upload\Storage\FileSystem($this->assetsDirectory)); + } + + public function testGetName() + { + $file = $this->getNewFile(); + $this->assertEquals('foo', $file->getName()); + } + + public function testGetNameWithExtension() + { + $file = $this->getNewFile(); + $this->assertEquals('foo.txt', $file->getNameWithExtension()); + } + + public function testGetNameWithExtensionUsingCustomName() + { + $file = $this->getNewFile(); + $file->setName('bar'); + $this->assertEquals('bar.txt', $file->getNameWithExtension()); + } + + public function testGetMimetype() + { + $file = $this->getNewFile(); + $this->assertEquals('text/plain', $file->getMimetype()); + } + + public function testAddValidationErrors() + { + $file = $this->getNewFile(); + $file->addError('Error'); + $this->assertEquals(1, count($file->getErrors())); + } + + public function testIsValidIfNoValidations() + { + $file = $this->getNewFile(); + $this->assertEmpty($file->getErrors()); + } + + public function testWillUploadIfNoValidations() + { + $file = $this->getNewFile(); + $this->assertTrue($file->upload()); + } + + public function testAddValidations() + { + $file = $this->getNewFile(); + $file->addValidations(new \Upload\Validation\Mimetype(array( + 'text/plain' + ))); + $this->assertEquals(1, count($file->getValidations())); + } + + public function testWillUploadWithPassingValidations() + { + $file = $this->getNewFile(); + $file->addValidations(new \Upload\Validation\Mimetype(array( + 'text/plain' + ))); + $this->assertTrue($file->upload()); + } + + /** + * @expectedException \Upload\Exception\UploadException + */ + public function testWillNotUploadWithFailingValidations() + { + $file = $this->getNewFile(); + $file->addValidations(new \Upload\Validation\Mimetype(array( + 'image/png' + ))); + $file->upload(); + } + + public function testPopulatesErrorsWithFailingValidations() + { + $file = $this->getNewFile(); + $file->addValidations(new \Upload\Validation\Mimetype(array( + 'image/png' + ))); + try { + $file->upload(); + } catch(\Upload\Exception\UploadException $e) { + $this->assertEquals(1, count($file->getErrors())); + } + } + + public function testValidationFailsIfUploadErrorCode() + { + $_FILES['foo']['error'] = 4; + $file = $this->getNewFile(); + $this->assertFalse($file->validate()); + } + + public function testValidationFailsIfNotUploadedFile() + { + $file = $this->getMock( + '\Upload\File', + array('isUploadedFile'), + array('foo', new \Upload\Storage\FileSystem($this->assetsDirectory)) + ); + $file->expects($this->any()) + ->method('isUploadedFile') + ->will($this->returnValue(false)); + $this->assertFalse($file->validate()); + } + + public function testParsesHumanFriendlyFileSizes() + { + $this->assertEquals(100, \Upload\File::humanReadableToBytes('100')); + $this->assertEquals(102400, \Upload\File::humanReadableToBytes('100K')); + $this->assertEquals(104857600, \Upload\File::humanReadableToBytes('100M')); + $this->assertEquals(107374182400, \Upload\File::humanReadableToBytes('100G')); + $this->assertEquals(100, \Upload\File::humanReadableToBytes('100F')); // <-- Unrecognized. Assume bytes. + } +} diff --git a/vendor/codeguy/upload/tests/Storage/FileSystemTest.php b/vendor/codeguy/upload/tests/Storage/FileSystemTest.php new file mode 100644 index 0000000..c7089f0 --- /dev/null +++ b/vendor/codeguy/upload/tests/Storage/FileSystemTest.php @@ -0,0 +1,79 @@ +assetsDirectory = dirname(__DIR__) . '/assets'; + + // Reset $_FILES superglobal + $_FILES['foo'] = array( + 'name' => 'foo.txt', + 'tmp_name' => $this->assetsDirectory . '/foo.txt', + 'error' => 0 + ); + } + + public function testInstantiationWithValidDirectory() + { + try { + $storage = $this->getMock( + '\Upload\Storage\FileSystem', + array('upload'), + array($this->assetsDirectory) + ); + } catch(\InvalidArgumentException $e) { + $this->fail('Unexpected argument thrown during instantiation with valid directory'); + } + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testInstantiationWithInvalidDirectory() + { + $storage = $this->getMock( + '\Upload\Storage\FileSystem', + array('upload'), + array('/foo') + ); + } + + /** + * Test won't overwrite existing file + * @expectedException \RuntimeException + */ + public function testWillNotOverwriteFile() + { + $storage = new \Upload\Storage\FileSystem($this->assetsDirectory, false); + $file = new \Upload\File('foo', $storage); + $file->upload(); + } + + /** + * Test will overwrite existing file + */ + public function testWillOverwriteFile() + { + $storage = $this->getMock( + '\Upload\Storage\FileSystem', + array('moveUploadedFile'), + array($this->assetsDirectory, true) + ); + $storage->expects($this->any()) + ->method('moveUploadedFile') + ->will($this->returnValue(true)); + $file = $this->getMock( + '\Upload\File', + array('isUploadedFile'), + array('foo', $storage) + ); + $file->expects($this->any()) + ->method('isUploadedFile') + ->will($this->returnValue(true)); + $this->assertTrue($file->upload()); + } +} diff --git a/vendor/codeguy/upload/tests/Validation/ExtensionTest.php b/vendor/codeguy/upload/tests/Validation/ExtensionTest.php new file mode 100644 index 0000000..617bb4e --- /dev/null +++ b/vendor/codeguy/upload/tests/Validation/ExtensionTest.php @@ -0,0 +1,43 @@ +assetsDirectory = dirname(__DIR__) . '/assets'; + + // Create stubbed storage instance + $this->storage = $this->getMock( + '\Upload\Storage\FileSystem', + array('upload'), + array($this->assetsDirectory) + ); + $this->storage->expects($this->any()) + ->method('upload') + ->will($this->returnValue(true)); + + // Reset $_FILES superglobal + $_FILES['foo'] = array( + 'name' => 'foo.txt', + 'tmp_name' => $this->assetsDirectory . '/foo.txt', + 'error' => 0 + ); + } + + public function testValidExtension() + { + $file = new \Upload\File('foo', $this->storage); + $validation = new \Upload\Validation\Extension('txt'); + $this->assertTrue($validation->validate($file)); + } + + public function testInvalidExtension() + { + $file = new \Upload\File('foo', $this->storage); + $validation = new \Upload\Validation\Extension('csv'); + $this->assertFalse($validation->validate($file)); + } +} diff --git a/vendor/codeguy/upload/tests/Validation/MimetypeTest.php b/vendor/codeguy/upload/tests/Validation/MimetypeTest.php new file mode 100644 index 0000000..675eebb --- /dev/null +++ b/vendor/codeguy/upload/tests/Validation/MimetypeTest.php @@ -0,0 +1,47 @@ +assetsDirectory = dirname(__DIR__) . '/assets'; + + // Create stubbed storage instance + $this->storage = $this->getMock( + '\Upload\Storage\FileSystem', + array('upload'), + array($this->assetsDirectory) + ); + $this->storage->expects($this->any()) + ->method('upload') + ->will($this->returnValue(true)); + + // Reset $_FILES superglobal + $_FILES['foo'] = array( + 'name' => 'foo.txt', + 'tmp_name' => $this->assetsDirectory . '/foo.txt', + 'error' => 0 + ); + } + + public function testValidMimetype() + { + $file = new \Upload\File('foo', $this->storage); + $validation = new \Upload\Validation\Mimetype(array( + 'text/plain' + )); + $this->assertTrue($validation->validate($file)); + } + + public function testInvalidMimetype() + { + $file = new \Upload\File('foo', $this->storage); + $validation = new \Upload\Validation\Mimetype(array( + 'image/png' + )); + $this->assertFalse($validation->validate($file)); + } +} diff --git a/vendor/codeguy/upload/tests/Validation/SizeTest.php b/vendor/codeguy/upload/tests/Validation/SizeTest.php new file mode 100644 index 0000000..d4aa89e --- /dev/null +++ b/vendor/codeguy/upload/tests/Validation/SizeTest.php @@ -0,0 +1,57 @@ +assetsDirectory = dirname(__DIR__) . '/assets'; + + // Create stubbed storage instance + $this->storage = $this->getMock( + '\Upload\Storage\FileSystem', + array('upload'), + array($this->assetsDirectory) + ); + $this->storage->expects($this->any()) + ->method('upload') + ->will($this->returnValue(true)); + + // Reset $_FILES superglobal + $_FILES['foo'] = array( + 'name' => 'foo.txt', + 'tmp_name' => $this->assetsDirectory . '/foo.txt', + 'error' => 0 + ); + } + + public function testValidFileSize() + { + $file = new \Upload\File('foo', $this->storage); + $validation = new \Upload\Validation\Size(500); + $this->assertTrue($validation->validate($file)); + } + + public function testValidFileSizeWithHumanReadableArgument() + { + $file = new \Upload\File('foo', $this->storage); + $validation = new \Upload\Validation\Size('500B'); + $this->assertTrue($validation->validate($file)); + } + + public function testInvalidFileSize() + { + $file = new \Upload\File('foo', $this->storage); + $validation = new \Upload\Validation\Size(400); + $this->assertFalse($validation->validate($file)); + } + + public function testInvalidFileSizeWithHumanReadableArgument() + { + $file = new \Upload\File('foo', $this->storage); + $validation = new \Upload\Validation\Size('400B'); + $this->assertFalse($validation->validate($file)); + } +} diff --git a/vendor/codeguy/upload/tests/assets/foo.txt b/vendor/codeguy/upload/tests/assets/foo.txt new file mode 100644 index 0000000..0dc0fba --- /dev/null +++ b/vendor/codeguy/upload/tests/assets/foo.txt @@ -0,0 +1,6 @@ +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod +tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, +quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo +consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse +cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non +proident, sunt in culpa qui officia deserunt mollit anim id est laborum. diff --git a/vendor/codeguy/upload/tests/bootstrap.php b/vendor/codeguy/upload/tests/bootstrap.php new file mode 100644 index 0000000..5372835 --- /dev/null +++ b/vendor/codeguy/upload/tests/bootstrap.php @@ -0,0 +1,3 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + + private $classMapAuthoritative = false; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 + if ('\\' == $class[0]) { + $class = substr($class, 1); + } + + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative) { + return false; + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if ($file === null && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if ($file === null) { + // Remember that this class does not exist. + return $this->classMap[$class] = false; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach ($this->prefixDirsPsr4[$prefix] as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000..1a28124 --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) 2016 Nils Adermann, Jordi Boggiano + +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: + +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/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..7a91153 --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,9 @@ + $vendorDir . '/nikic/fast-route/src/functions.php', +); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..7c2cb36 --- /dev/null +++ b/vendor/composer/autoload_namespaces.php @@ -0,0 +1,15 @@ + array($vendorDir . '/zendframework/zendxml/library'), + 'Upload' => array($vendorDir . '/codeguy/upload/src'), + 'Pimple' => array($vendorDir . '/pimple/pimple/src'), + 'PicoFeed' => array($vendorDir . '/fguillot/picofeed/lib'), + 'Parsedown' => array($vendorDir . '/erusev/parsedown'), + 'OAuth2' => array($vendorDir . '/bshaffer/oauth2-server-php/src'), +); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php new file mode 100644 index 0000000..cc46458 --- /dev/null +++ b/vendor/composer/autoload_psr4.php @@ -0,0 +1,14 @@ + array($vendorDir . '/symfony/yaml'), + 'Slim\\' => array($vendorDir . '/slim/slim/Slim'), + 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'), + 'Interop\\Container\\' => array($vendorDir . '/container-interop/container-interop/src/Interop/Container'), + 'FastRoute\\' => array($vendorDir . '/nikic/fast-route/src'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 0000000..71d78f8 --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,59 @@ + $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + + $loader->register(true); + + $includeFiles = require __DIR__ . '/autoload_files.php'; + foreach ($includeFiles as $fileIdentifier => $file) { + composerRequired1db2574a85c0ba6f142743249ba228f($fileIdentifier, $file); + } + + return $loader; + } +} + +function composerRequired1db2574a85c0ba6f142743249ba228f($fileIdentifier, $file) +{ + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + require $file; + + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000..dac54c6 --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,533 @@ +[ + { + "name": "erusev/parsedown", + "version": "1.6.0", + "version_normalized": "1.6.0.0", + "source": { + "type": "git", + "url": "https://github.com/erusev/parsedown.git", + "reference": "3ebbd730b5c2cf5ce78bc1bf64071407fc6674b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/3ebbd730b5c2cf5ce78bc1bf64071407fc6674b7", + "reference": "3ebbd730b5c2cf5ce78bc1bf64071407fc6674b7", + "shasum": "" + }, + "time": "2015-10-04 16:44:32", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "Parsedown": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "description": "Parser for Markdown.", + "homepage": "http://parsedown.org", + "keywords": [ + "markdown", + "parser" + ] + }, + { + "name": "zendframework/zendxml", + "version": "1.0.2", + "version_normalized": "1.0.2.0", + "source": { + "type": "git", + "url": "https://github.com/zendframework/ZendXml.git", + "reference": "7b64507bc35d841c9c5802d67f6f87ef8e1a58c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/ZendXml/zipball/7b64507bc35d841c9c5802d67f6f87ef8e1a58c9", + "reference": "7b64507bc35d841c9c5802d67f6f87ef8e1a58c9", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^3.7 || ^4.0", + "squizlabs/php_codesniffer": "^1.5" + }, + "time": "2016-02-04 21:02:08", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-0": { + "ZendXml\\": "library/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Utility library for XML usage, best practices, and security in PHP", + "homepage": "http://packages.zendframework.com/", + "keywords": [ + "security", + "xml", + "zf2" + ] + }, + { + "name": "fguillot/picofeed", + "version": "v0.1.23", + "version_normalized": "0.1.23.0", + "source": { + "type": "git", + "url": "https://github.com/fguillot/picoFeed.git", + "reference": "a7c3d420c239fe9ffc39b0d06b6e57db39ce3797" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fguillot/picoFeed/zipball/a7c3d420c239fe9ffc39b0d06b6e57db39ce3797", + "reference": "a7c3d420c239fe9ffc39b0d06b6e57db39ce3797", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "php": ">=5.3.0", + "zendframework/zendxml": "^1.0" + }, + "suggest": { + "ext-curl": "PicoFeed will use cURL if present" + }, + "time": "2016-04-17 22:31:55", + "bin": [ + "picofeed" + ], + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "PicoFeed": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frédéric Guillot" + } + ], + "description": "Modern library to handle RSS/Atom feeds", + "homepage": "https://github.com/fguillot/picoFeed" + }, + { + "name": "container-interop/container-interop", + "version": "1.1.0", + "version_normalized": "1.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/container-interop/container-interop.git", + "reference": "fc08354828f8fd3245f77a66b9e23a6bca48297e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/container-interop/container-interop/zipball/fc08354828f8fd3245f77a66b9e23a6bca48297e", + "reference": "fc08354828f8fd3245f77a66b9e23a6bca48297e", + "shasum": "" + }, + "time": "2014-12-30 15:22:37", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Interop\\Container\\": "src/Interop/Container/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Promoting the interoperability of container objects (DIC, SL, etc.)" + }, + { + "name": "nikic/fast-route", + "version": "v0.6.0", + "version_normalized": "0.6.0.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "31fa86924556b80735f98b294a7ffdfb26789f22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/31fa86924556b80735f98b294a7ffdfb26789f22", + "reference": "31fa86924556b80735f98b294a7ffdfb26789f22", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "time": "2015-06-18 19:15:47", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "FastRoute\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ] + }, + { + "name": "psr/http-message", + "version": "1.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", + "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2015-05-04 20:22:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ] + }, + { + "name": "pimple/pimple", + "version": "v3.0.2", + "version_normalized": "3.0.2.0", + "source": { + "type": "git", + "url": "https://github.com/silexphp/Pimple.git", + "reference": "a30f7d6e57565a2e1a316e1baf2a483f788b258a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a30f7d6e57565a2e1a316e1baf2a483f788b258a", + "reference": "a30f7d6e57565a2e1a316e1baf2a483f788b258a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2015-09-11 15:10:35", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-0": { + "Pimple": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Pimple, a simple Dependency Injection Container", + "homepage": "http://pimple.sensiolabs.org", + "keywords": [ + "container", + "dependency injection" + ] + }, + { + "name": "slim/slim", + "version": "3.3.0", + "version_normalized": "3.3.0.0", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim.git", + "reference": "939f2e85d57508de9cff241d10091cd972f221c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/939f2e85d57508de9cff241d10091cd972f221c3", + "reference": "939f2e85d57508de9cff241d10091cd972f221c3", + "shasum": "" + }, + "require": { + "container-interop/container-interop": "^1.1", + "nikic/fast-route": "^0.6", + "php": ">=5.5.0", + "pimple/pimple": "^3.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0", + "squizlabs/php_codesniffer": "^2.5" + }, + "time": "2016-03-10 21:37:40", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Slim\\": "Slim" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "http://akrabat.com" + }, + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Gabriel Manricks", + "email": "gmanricks@me.com", + "homepage": "http://gabrielmanricks.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "http://silentworks.co.uk" + } + ], + "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", + "homepage": "http://slimframework.com", + "keywords": [ + "api", + "framework", + "micro", + "router" + ] + }, + { + "name": "symfony/yaml", + "version": "v3.0.4", + "version_normalized": "3.0.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "0047c8366744a16de7516622c5b7355336afae96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/0047c8366744a16de7516622c5b7355336afae96", + "reference": "0047c8366744a16de7516622c5b7355336afae96", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "time": "2016-03-04 07:55:57", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "installation-source": "dist", + "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" + }, + { + "name": "codeguy/upload", + "version": "1.3.2", + "version_normalized": "1.3.2.0", + "source": { + "type": "git", + "url": "https://github.com/codeguy/Upload.git", + "reference": "6a9e5e1fb58d65346d0e557db2d46fb25efd3e37" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/codeguy/Upload/zipball/6a9e5e1fb58d65346d0e557db2d46fb25efd3e37", + "reference": "6a9e5e1fb58d65346d0e557db2d46fb25efd3e37", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, + "time": "2013-07-07 17:01:41", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "Upload": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "info@joshlockhart.com", + "homepage": "http://www.joshlockhart.com/" + } + ], + "description": "Handle file uploads with extensible validation and storage strategies", + "homepage": "http://github.com/codeguy/Upload", + "keywords": [ + "file", + "upload", + "validation" + ] + }, + { + "name": "bshaffer/oauth2-server-php", + "version": "v1.8.0", + "version_normalized": "1.8.0.0", + "source": { + "type": "git", + "url": "https://github.com/bshaffer/oauth2-server-php.git", + "reference": "058c98f73209f9c49495e1799d32c035196fe8b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bshaffer/oauth2-server-php/zipball/058c98f73209f9c49495e1799d32c035196fe8b8", + "reference": "058c98f73209f9c49495e1799d32c035196fe8b8", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "suggest": { + "aws/aws-sdk-php": "~2.8 is required to use the DynamoDB storage engine", + "firebase/php-jwt": "~2.2 is required to use JWT features", + "predis/predis": "Required to use the Redis storage engine", + "thobbs/phpcassa": "Required to use the Cassandra storage engine" + }, + "time": "2015-09-18 18:05:10", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "OAuth2": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brent Shaffer", + "email": "bshafs@gmail.com", + "homepage": "http://brentertainment.com" + } + ], + "description": "OAuth2 Server for PHP", + "homepage": "http://github.com/bshaffer/oauth2-server-php", + "keywords": [ + "auth", + "oauth", + "oauth2" + ] + } +] diff --git a/vendor/container-interop/container-interop/.gitignore b/vendor/container-interop/container-interop/.gitignore new file mode 100644 index 0000000..b2395aa --- /dev/null +++ b/vendor/container-interop/container-interop/.gitignore @@ -0,0 +1,3 @@ +composer.lock +composer.phar +/vendor/ diff --git a/vendor/container-interop/container-interop/LICENSE b/vendor/container-interop/container-interop/LICENSE new file mode 100644 index 0000000..7671d90 --- /dev/null +++ b/vendor/container-interop/container-interop/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 container-interop + +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: + +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/vendor/container-interop/container-interop/README.md b/vendor/container-interop/container-interop/README.md new file mode 100644 index 0000000..ec434d0 --- /dev/null +++ b/vendor/container-interop/container-interop/README.md @@ -0,0 +1,85 @@ +# Container Interoperability + +[![Latest Stable Version](https://poser.pugx.org/container-interop/container-interop/v/stable.png)](https://packagist.org/packages/container-interop/container-interop) + +*container-interop* tries to identify and standardize features in *container* objects (service locators, +dependency injection containers, etc.) to achieve interopererability. + +Through discussions and trials, we try to create a standard, made of common interfaces but also recommendations. + +If PHP projects that provide container implementations begin to adopt these common standards, then PHP +applications and projects that use containers can depend on the common interfaces instead of specific +implementations. This facilitates a high-level of interoperability and flexibility that allows users to consume +*any* container implementation that can be adapted to these interfaces. + +The work done in this project is not officially endorsed by the [PHP-FIG](http://www.php-fig.org/), but it is being +worked on by members of PHP-FIG and other good developers. We adhere to the spirit and ideals of PHP-FIG, and hope +this project will pave the way for one or more future PSRs. + + +## Installation + +You can install this package through Composer: + +```json +{ + "require": { + "container-interop/container-interop": "~1.0" + } +} +``` + +The packages adheres to the [SemVer](http://semver.org/) specification, and there will be full backward compatibility +between minor versions. + +## Standards + +### Available + +- [`ContainerInterface`](src/Interop/Container/ContainerInterface.php). +[Description](docs/ContainerInterface.md) [Meta Document](docs/ContainerInterface-meta.md). +Describes the interface of a container that exposes methods to read its entries. +- [*Delegate lookup feature*](docs/Delegate-lookup.md). +[Meta Document](docs/Delegate-lookup-meta.md). +Describes the ability for a container to delegate the lookup of its dependencies to a third-party container. This +feature lets several containers work together in a single application. + +### Proposed + +View open [request for comments](https://github.com/container-interop/container-interop/labels/RFC) + +## Compatible projects + +### Projects implementing `ContainerInterface` + +- [Acclimate](https://github.com/jeremeamia/acclimate-container) +- [dcp-di](https://github.com/estelsmith/dcp-di) +- [Mouf](http://mouf-php.com) +- [Njasm Container](https://github.com/njasm/container) +- [PHP-DI](http://php-di.org) +- [PimpleInterop](https://github.com/moufmouf/pimple-interop) +- [XStatic](https://github.com/jeremeamia/xstatic) + +### Projects implementing the *delegate lookup* feature + +- [Mouf](http://mouf-php.com) +- [PHP-DI](http://php-di.org) +- [PimpleInterop](https://github.com/moufmouf/pimple-interop) + +## Workflow + +Everyone is welcome to join and contribute. + +The general workflow looks like this: + +1. Someone opens a discussion (GitHub issue) to suggest an interface +1. Feedback is gathered +1. The interface is added to a development branch +1. We release alpha versions so that the interface can be experimented with +1. Discussions and edits ensue until the interface is deemed stable by a general consensus +1. A new minor version of the package is released + +We try to not break BC by creating new interfaces instead of editing existing ones. + +While we currently work on interfaces, we are open to anything that might help towards interoperability, may that +be code, best practices, etc. diff --git a/vendor/container-interop/container-interop/composer.json b/vendor/container-interop/container-interop/composer.json new file mode 100644 index 0000000..84f3875 --- /dev/null +++ b/vendor/container-interop/container-interop/composer.json @@ -0,0 +1,11 @@ +{ + "name": "container-interop/container-interop", + "type": "library", + "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", + "license": "MIT", + "autoload": { + "psr-4": { + "Interop\\Container\\": "src/Interop/Container/" + } + } +} diff --git a/vendor/container-interop/container-interop/docs/ContainerInterface-meta.md b/vendor/container-interop/container-interop/docs/ContainerInterface-meta.md new file mode 100644 index 0000000..90711c9 --- /dev/null +++ b/vendor/container-interop/container-interop/docs/ContainerInterface-meta.md @@ -0,0 +1,114 @@ +# ContainerInterface Meta Document + +## Introduction + +This document describes the process and discussions that lead to the `ContainerInterface`. +Its goal is to explain the reasons behind each decision. + +## Goal + +The goal set by `ContainerInterface` is to standardize how frameworks and libraries make use of a +container to obtain objects and parameters. + +By standardizing such a behavior, frameworks and libraries using the `ContainerInterface` +could work with any compatible container. +That would allow end users to choose their own container based on their own preferences. + +It is important to distinguish the two usages of a container: + +- configuring entries +- fetching entries + +Most of the time, those two sides are not used by the same party. +While it is often end users who tend to configure entries, it is generally the framework that fetch +entries to build the application. + +This is why this interface focuses only on how entries can be fetched from a container. + +## Interface name + +The interface name has been thoroughly discussed and was decided by a vote. + +The list of options considered with their respective votes are: + +- `ContainerInterface`: +8 +- `ProviderInterface`: +2 +- `LocatorInterface`: 0 +- `ReadableContainerInterface`: -5 +- `ServiceLocatorInterface`: -6 +- `ObjectFactory`: -6 +- `ObjectStore`: -8 +- `ConsumerInterface`: -9 + +[Full results of the vote](https://github.com/container-interop/container-interop/wiki/%231-interface-name:-Vote) + +The complete discussion can be read in [the issue #1](https://github.com/container-interop/container-interop/issues/1). + +## Interface methods + +The choice of which methods the interface would contain was made after a statistical analysis of existing containers. +The results of this analysis are available [in this document](https://gist.github.com/mnapoli/6159681). + +The summary of the analysis showed that: + +- all containers offer a method to get an entry by its id +- a large majority name such method `get()` +- for all containers, the `get()` method has 1 mandatory parameter of type string +- some containers have an optional additional argument for `get()`, but it doesn't same the same purpose between containers +- a large majority of the containers offer a method to test if it can return an entry by its id +- a majority name such method `has()` +- for all containers offering `has()`, the method has exactly 1 parameter of type string +- a large majority of the containers throw an exception rather than returning null when an entry is not found in `get()` +- a large majority of the containers don't implement `ArrayAccess` + +The question of whether to include methods to define entries has been discussed in +[issue #1](https://github.com/container-interop/container-interop/issues/1). +It has been judged that such methods do not belong in the interface described here because it is out of its scope +(see the "Goal" section). + +As a result, the `ContainerInterface` contains two methods: + +- `get()`, returning anything, with one mandatory string parameter. Should throw an exception if the entry is not found. +- `has()`, returning a boolean, with one mandatory string parameter. + +### Number of parameters in `get()` method + +While `ContainerInterface` only defines one mandatory parameter in `get()`, it is not incompatible with +existing containers that have additional optional parameters. PHP allows an implementation to offer more parameters +as long as they are optional, because the implementation *does* satisfy the interface. + +This issue has been discussed in [issue #6](https://github.com/container-interop/container-interop/issues/6). + +### Type of the `$id` parameter + +The type of the `$id` parameter in `get()` and `has()` has been discussed in +[issue #6](https://github.com/container-interop/container-interop/issues/6). +While `string` is used in all the containers that were analyzed, it was suggested that allowing +anything (such as objects) could allow containers to offer a more advanced query API. + +An example given was to use the container as an object builder. The `$id` parameter would then be an +object that would describe how to create an instance. + +The conclusion of the discussion was that this was beyond the scope of getting entries from a container without +knowing how the container provided them, and it was more fit for a factory. + +## Contributors + +Are listed here all people that contributed in the discussions or votes, by alphabetical order: + +- [Amy Stephen](https://github.com/AmyStephen) +- [David Négrier](https://github.com/moufmouf) +- [Don Gilbert](https://github.com/dongilbert) +- [Jason Judge](https://github.com/judgej) +- [Jeremy Lindblom](https://github.com/jeremeamia) +- [Marco Pivetta](https://github.com/Ocramius) +- [Matthieu Napoli](https://github.com/mnapoli) +- [Paul M. Jones](https://github.com/pmjones) +- [Stephan Hochdörfer](https://github.com/shochdoerfer) +- [Taylor Otwell](https://github.com/taylorotwell) + +## Relevant links + +- [`ContainerInterface.php`](https://github.com/container-interop/container-interop/blob/master/src/Interop/Container/ContainerInterface.php) +- [List of all issues](https://github.com/container-interop/container-interop/issues?labels=ContainerInterface&milestone=&page=1&state=closed) +- [Vote for the interface name](https://github.com/container-interop/container-interop/wiki/%231-interface-name:-Vote) diff --git a/vendor/container-interop/container-interop/docs/ContainerInterface.md b/vendor/container-interop/container-interop/docs/ContainerInterface.md new file mode 100644 index 0000000..9f60967 --- /dev/null +++ b/vendor/container-interop/container-interop/docs/ContainerInterface.md @@ -0,0 +1,153 @@ +Container interface +=================== + +This document describes a common interface for dependency injection containers. + +The goal set by `ContainerInterface` is to standardize how frameworks and libraries make use of a +container to obtain objects and parameters (called *entries* in the rest of this document). + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", +"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be +interpreted as described in [RFC 2119][]. + +The word `implementor` in this document is to be interpreted as someone +implementing the `ContainerInterface` in a depency injection-related library or framework. +Users of dependency injections containers (DIC) are refered to as `user`. + +[RFC 2119]: http://tools.ietf.org/html/rfc2119 + +1. Specification +----------------- + +### 1.1 Basics + +- The `Interop\Container\ContainerInterface` exposes two methods : `get` and `has`. + +- `get` takes one mandatory parameter: an entry identifier. It MUST be a string. + A call to `get` can return anything (a *mixed* value), or throws an exception if the identifier + is not known to the container. Two successive calls to `get` with the same + identifier SHOULD return the same value. However, depending on the `implementor` + design and/or `user` configuration, different values might be returned, so + `user` SHOULD NOT rely on getting the same value on 2 successive calls. + While `ContainerInterface` only defines one mandatory parameter in `get()`, implementations + MAY accept additional optional parameters. + +- `has` takes one unique parameter: an entry identifier. It MUST return `true` + if an entry identifier is known to the container and `false` if it is not. + +### 1.2 Exceptions + +Exceptions directly thrown by the container MUST implement the +[`Interop\Container\Exception\ContainerException`](../src/Interop/Container/Exception/ContainerException.php). + +A call to the `get` method with a non-existing id should throw a +[`Interop\Container\Exception\NotFoundException`](../src/Interop/Container/Exception/NotFoundException.php). + +### 1.3 Additional features + +This section describes additional features that MAY be added to a container. Containers are not +required to implement these features to respect the ContainerInterface. + +#### 1.3.1 Delegate lookup feature + +The goal of the *delegate lookup* feature is to allow several containers to share entries. +Containers implementing this feature can perform dependency lookups in other containers. + +Containers implementing this feature will offer a greater lever of interoperability +with other containers. Implementation of this feature is therefore RECOMMENDED. + +A container implementing this feature: + +- MUST implement the `ContainerInterface` +- MUST provide a way to register a delegate container (using a constructor parameter, or a setter, + or any possible way). The delegate container MUST implement the `ContainerInterface`. + +When a container is configured to use a delegate container for dependencies: + +- Calls to the `get` method should only return an entry if the entry is part of the container. + If the entry is not part of the container, an exception should be thrown + (as requested by the `ContainerInterface`). +- Calls to the `has` method should only return `true` if the entry is part of the container. + If the entry is not part of the container, `false` should be returned. +- If the fetched entry has dependencies, **instead** of performing + the dependency lookup in the container, the lookup is performed on the *delegate container*. + +Important! By default, the lookup SHOULD be performed on the delegate container **only**, not on the container itself. + +It is however allowed for containers to provide exception cases for special entries, and a way to lookup +into the same container (or another container) instead of the delegate container. + +2. Package +---------- + +The interfaces and classes described as well as relevant exception are provided as part of the +[container-interop/container-interop](https://packagist.org/packages/container-interop/container-interop) package. + +3. `Interop\Container\ContainerInterface` +----------------------------------------- + +```php +setParentContainer($this); + } + } + ... + } +} + +``` + +**Cons:** + +Cons have been extensively discussed [here](https://github.com/container-interop/container-interop/pull/8#issuecomment-51721777). +Basically, forcing a setter into an interface is a bad idea. Setters are similar to constructor arguments, +and it's a bad idea to standardize a constructor: how the delegate container is configured into a container is an implementation detail. This outweights the benefits of the interface. + +### 4.4 Alternative: no exception case for delegate lookups + +Originally, the proposed wording for delegate lookup calls was: + +> Important! The lookup MUST be performed on the delegate container **only**, not on the container itself. + +This was later replaced by: + +> Important! By default, the lookup SHOULD be performed on the delegate container **only**, not on the container itself. +> +> It is however allowed for containers to provide exception cases for special entries, and a way to lookup +> into the same container (or another container) instead of the delegate container. + +Exception cases have been allowed to avoid breaking dependencies with some services that must be provided +by the container (on @njasm proposal). This was proposed here: https://github.com/container-interop/container-interop/pull/20#issuecomment-56597235 + +### 4.5 Alternative: having one of the containers act as the composite container + +In real-life scenarios, we usually have a big framework (Symfony 2, Zend Framework 2, etc...) and we want to +add another DI container to this container. Most of the time, the "big" framework will be responsible for +creating the controller's instances, using it's own DI container. Until *container-interop* is fully adopted, +the "big" framework will not be aware of the existence of a composite container that it should use instead +of its own container. + +For this real-life use cases, @mnapoli and @moufmouf proposed to extend the "big" framework's DI container +to make it act as a composite container. + +This has been discussed [here](https://github.com/container-interop/container-interop/pull/8#issuecomment-40367194) +and [here](http://mouf-php.com/container-interop-whats-next#solution4). + +This was implemented in Symfony 2 using: + +- [interop.symfony.di](https://github.com/thecodingmachine/interop.symfony.di/tree/v0.1.0) +- [framework interop](https://github.com/mnapoli/framework-interop/) + +This was implemented in Silex using: + +- [interop.silex.di](https://github.com/thecodingmachine/interop.silex.di) + +Having a container act as the composite container is not part of the delegate lookup standard because it is +simply a temporary design pattern used to make existing frameworks that do not support yet ContainerInterop +play nice with other DI containers. + + +5. Implementations +------------------ + +The following projects already implement the delegate lookup feature: + +- [Mouf](http://mouf-php.com), through the [`setDelegateLookupContainer` method](https://github.com/thecodingmachine/mouf/blob/2.0/src/Mouf/MoufManager.php#L2120) +- [PHP-DI](http://php-di.org/), through the [`$wrapperContainer` parameter of the constructor](https://github.com/mnapoli/PHP-DI/blob/master/src/DI/Container.php#L72) +- [pimple-interop](https://github.com/moufmouf/pimple-interop), through the [`$container` parameter of the constructor](https://github.com/moufmouf/pimple-interop/blob/master/src/Interop/Container/Pimple/PimpleInterop.php#L62) + +6. People +--------- + +Are listed here all people that contributed in the discussions, by alphabetical order: + +- [Alexandru Pătrănescu](https://github.com/drealecs) +- [Ben Peachey](https://github.com/potherca) +- [David Négrier](https://github.com/moufmouf) +- [Jeremy Lindblom](https://github.com/jeremeamia) +- [Marco Pivetta](https://github.com/Ocramius) +- [Matthieu Napoli](https://github.com/mnapoli) +- [Nelson J Morais](https://github.com/njasm) +- [Phil Sturgeon](https://github.com/philsturgeon) +- [Stephan Hochdörfer](https://github.com/shochdoerfer) + +7. Relevant Links +----------------- + +_**Note:** Order descending chronologically._ + +- [Pull request on the delegate lookup feature](https://github.com/container-interop/container-interop/pull/20) +- [Pull request on the interface idea](https://github.com/container-interop/container-interop/pull/8) +- [Original article exposing the delegate lookup idea along many others](http://mouf-php.com/container-interop-whats-next) + diff --git a/vendor/container-interop/container-interop/docs/Delegate-lookup.md b/vendor/container-interop/container-interop/docs/Delegate-lookup.md new file mode 100644 index 0000000..04eb3ae --- /dev/null +++ b/vendor/container-interop/container-interop/docs/Delegate-lookup.md @@ -0,0 +1,60 @@ +Delegate lookup feature +======================= + +This document describes a standard for dependency injection containers. + +The goal set by the *delegate lookup* feature is to allow several containers to share entries. +Containers implementing this feature can perform dependency lookups in other containers. + +Containers implementing this feature will offer a greater lever of interoperability +with other containers. Implementation of this feature is therefore RECOMMENDED. + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", +"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be +interpreted as described in [RFC 2119][]. + +The word `implementor` in this document is to be interpreted as someone +implementing the delegate lookup feature in a dependency injection-related library or framework. +Users of dependency injections containers (DIC) are refered to as `user`. + +[RFC 2119]: http://tools.ietf.org/html/rfc2119 + +1. Vocabulary +------------- + +In a dependency injection container, the container is used to fetch entries. +Entries can have dependencies on other entries. Usually, these other entries are fetched by the container. + +The *delegate lookup* feature is the ability for a container to fetch dependencies in +another container. In the rest of the document, the word "container" will reference the container +implemented by the implementor. The word "delegate container" will reference the container we are +fetching the dependencies from. + +2. Specification +---------------- + +A container implementing the *delegate lookup* feature: + +- MUST implement the [`ContainerInterface`](ContainerInterface.md) +- MUST provide a way to register a delegate container (using a constructor parameter, or a setter, + or any possible way). The delegate container MUST implement the [`ContainerInterface`](ContainerInterface.md). + +When a container is configured to use a delegate container for dependencies: + +- Calls to the `get` method should only return an entry if the entry is part of the container. + If the entry is not part of the container, an exception should be thrown + (as requested by the [`ContainerInterface`](ContainerInterface.md)). +- Calls to the `has` method should only return `true` if the entry is part of the container. + If the entry is not part of the container, `false` should be returned. +- If the fetched entry has dependencies, **instead** of performing + the dependency lookup in the container, the lookup is performed on the *delegate container*. + +Important: By default, the dependency lookups SHOULD be performed on the delegate container **only**, not on the container itself. + +It is however allowed for containers to provide exception cases for special entries, and a way to lookup +into the same container (or another container) instead of the delegate container. + +3. Package / Interface +---------------------- + +This feature is not tied to any code, interface or package. diff --git a/vendor/container-interop/container-interop/docs/images/interoperating_containers.png b/vendor/container-interop/container-interop/docs/images/interoperating_containers.png new file mode 100644 index 0000000000000000000000000000000000000000..9c672e16c72a08708ea5bdeb51757c2951d3ee66 GIT binary patch literal 35971 zcmbSybyQSe)Ha~B#L&_)bW0DVbV*5fgOniM-5m-_2`CIDB_Z7*AR-OY(%mKSp837& zUElwoYq5lxbI(0zpS_>`>}TH_qpm8CgGG*ogoK2ns34<>goI)UexMj=;1kyB18eZ# zV>c;9Ee!C_AHymd{Eg|Npznr+gxiDoL4L=MOM!$$i=-&?Ld!dQZ^6r7OM6lBXf#J6 zO~scv;{C5se%8#3_r_Nc^0to?;hjnTRvn0g}1g$d6&BaEty%YORqNr4@H*z5~1PvwDk1!1zh=> zFeJn;>U~ym(b3UxY~ES$C;g6;N?HLb*RkhEq+4b)7ZAsk4jHY{0Gy&e0%&pm0;<7>gHumv!y1PRqXY$jH*si=eVH$aQ3=qPV?%rWu(OtW==`lcC?C2Ugh%6}VK=-q`3e(2l6N zyG?=l^2^;{pegLD(08S!&WS&sAxYZ(=L`Rt!OB!5eXK(zf#q1h@Z zNp=JOL^ zoC&6=-q3#u6zcVdw(0xxQ9u6t;hR#f`*LB0+@qfwH>FZty(M4)E%3dJTy%_NWg;Xj7DnT3|T7X`GCT!!X6enmuiS!i`u{_8-VASKP;=)p{< znZZ-m;4M}An^zm<^fy7s{EV}q{`N5vJr?C2u$^G9jg3^{boXqcm2$;6ovVW2@Zeyy zg5rzSWjdbT-WL=kQ#dJT8R4B_DLmd)OBbuz3oP>uc-FU<`Ho8xdhG1h;^ucns@4#3 zjbw`&*$L_N!=^;%6pF|W<1M%)X);68^kTGJqOBU8{ zd0Oi$jHUC)-$CLK^1|DWcR!S<3(Qd0amVwFa0r`8dj)6IWO)*Ozr$53*_kSeyZV6Wf zIj_+J@Bb3Er%_D6sL)J(h|{C@nE=cgrGu{6)`=z0;qh-bUXK5vQIC@p0!Hq&Ed9Bem~7&-;Sl>$4Dc&6C$XooyAs~h{*(t{=ncc zZMMIbM8`pg99H=;?uG6@0wyNC31oEL@{YtW53k)Y^Wopzhl6&V>|SQGg~Dq=6k|Q3 zE+*^`GD->@FRHx^`I*(f-4jA=YPR`%+PqA{SY2}S`(_I70fB;;dny_5mKL-gv~F0*5xD`xm~oWr6&MJX;v zuhhgWz_LVuBV>gI>9`Rre_e7p3!HVp$UXl%52Cwo;|-iacfMyi&tyB{@M2{OKDllD zXekoE`sk(@a9>G@TQACo74-IkzhIIJX9y&M1)P@$Z_;>-I88t_)x&krQMNh#Z}{00 z?;KV5lSt>pM35KA(>M#b+~4l%I2xREJh(+=Dj?mvw8Vw5)pdKy;;M zmS=Nz6EfaV`I`=c$`3#(XYc4oW%YG0W7Xu;&E3m4f|kQHBo+fLJ}NaVEI!$)<-S{L z!qpzw;r(IJf31cC&X+LMRJ36H1;M>maqu)@4iZQI1O2}hCX+UArh-K>Ecd$RA-Tz) zp1aHBFnCQiT&6%&$J<T(+>2V&-Vc*hOyY8A6=7`cakar zXS@gTEL+wi*%2WbrpWI$+y~!Sn^5a42GNiY4H3u3v$%i^x&v*MOYEvLHAVlXCuKYA zgw6Xf(vTwNNR}~`pUiuobopttCngYcx*^CH>7ogS%D{l7LMaE`k#UZCl(*nd*g}mL zcznd5KrN`UogCd1KEKl873A}zU&t|ddjG;nje-x2@BUJ8i-DdfK9QvgIMI$3NE*Mg zMt->jh+w&bY1kk9AhBNiq0?)}PNN@1u-m<_Aopm|sw+J78qC;nacioPzK;`3lAfxG zMfB`BDscY%Is)PdH{gy~j4LB_41$VCl``&&Pau%YyJ5HJQF2=4MnP^LWNrDAfA5Rv zcMu^08AIijkujTF?X7`vHgty^QU_7uk_;098^A~%wBP-v!$)n|I(wqhcquW9?+fye zmpt>A>IJ!>gCt}6dP+)Fd-aCLcMtb35uUObtY3S#v21Zi0s$_@R|+COw)s%T z*qjbG@SL&!e%JboG(DF0+TnN7u~xs^jV$FhJF?&XiD`^xGU7PJfwcD0*3MjQ|JIBH zcPW1Q?z~30em*=pRC+e#(bY|xAJ0|kce3;5C20p97Ak_20T{7)G`u(*gWX(!f1I7s}j`8$;c4d3!gta zSHO)!eRF^E)|(2K|KTx6j*T|kSv+#`?1*fj-olJ-Ux$VnhF;73K?(%-PBoU-LB~!M zO=xQywS5k*Zu`YUGLK#IZ*xdQ%c{7@%-xhBNQ$Bb&z|udE^NSQN-kB*+;BJ(}=sJ@lPS zVHjAdLN;kI?1V=*;Ubvoj*jbx4_o4{^jh|HZ|1*xEr#~5sPnKDd<6>wsAX)}YS=&9 zEK3(VNCJU)G&c^bSdC<(YR45^EgZIuEOKHgB6zO?r0~smVp#30S5B)TQ7OXDQ|LjK z8vt=+Uoq9N-%&HV;tC^mZ z42f#$+b`H8TCe4Z_HPt{k8eEzFq7iC{iTMF@G|g(*P(&sGY^o36M8Wc5LS**JYu!U;F~siaX;b2 za*bheu`=>yT}8(|J9qw0l~^>#dEu-PZB`>oS8O+}CVbzGSK~x@_&_t8Eg?fB3{~vv z5YZjLT6fc}3>~-E2DbVMY$f&ceTIq55ToQ;P0g=O(sp$&T6$RV@CQD~`(z}58pBWd zT(3_bg6W=VZdXOaP&4$3liynHlWS4Jpvv`J-*1!DOT3L<5DoDAiWiCB>xTFCbrI2U zvu846d8*X2eBKMZq?$cnOk>KQhQ|@dMn=QvT?g>LNYm~Ri2W%}PI*{Mf{Xdmu7EIH9iEnW)#b@@(f1^{=WQIyD z3TUBX)PS$94R|@^Pi^!3YY!}RSCE;Aah#kKzUjVn(XVK<>#S4Xbytd#nVpJ zr56yyi^^6p)>s+DP zN-Tc4sID3v~&A!;>5vL|581n&8%#%+7o`^~)= zg0#eX9cscT>35i%a2Rzcg_jLQLRpd#gWg)5+XH|EY$Gt3`=E(1N++-aVKkgMawU#G z-^0K{hyX_Xs!C7)1bYw;F7m+Fwioa z!8|$|;UzoHPuG|SeCvPUQHK|{nRG@+&&d-=MIZRqbVuQ>{mPdf7#s}2NAbHnkacq6 zY~p>4Wsw1MR5n#7Rn&3YUvBibEyFMF$a}&K#4J)L*V&>oTdWYksje%kx1c=VHb< z)lq~Jx8-K1Os~cY^W^mOgV`ABE-7`SA39DrR52Nfsf~+HJv3Y9t|v1w^6|E^Q8)i9 z5d;!)eY&}pB2WE7P7Y106uYcx=j>y}w22>1z|C1_z~w^JT7MGXSb-|<)dcI(NUMR* zIY|Tv0!9p;v4XFwWu zaOEvV%#w<}pLi3`5bkxLp`j6Sd)Q7RwB*mHnkzQDVze2WRv{ZlvT`y1#x_;f*v2K@ zAEWx0eMAEGF9L{$(CuFBC{NJ6KPAi^F5Mlwnd3M)HD&QLTf}=Mf;7`kBZHihl}5^{ z6+k|#^?bckCa)!0z2hv3mnie3_@UKq`~7ZvmHlMtX{)G4v$b5N_4Ee>;-|*O#=0zc zOq-A9itq2Nz$&dA#tLMwz#{0v%_>PeSX-fV^^;Jy@7cT`0X{*)pPQT0Ik%qc{N3El zJH4P%t$)vXDfS2|SDBQ% z1SKwduJ>vTA1-tKuHm+e@X^`0n%h+}2zj}PZsbaQb!7D%*oUHXOaYmN7Z@RxZ8un= zo_oP%392EUjD~xY;B=p_k)WU^=c!SJ) zH>^^xAR>iPZrn_QgM(AE4+iN}81(+{AXJPyyso{APM#w_~uuR-DK_LkX5K+ zg$@t>&NdrR9Hs?qn~!4%nVa~@3iv)`y^lp}py;O{S9h-MUd3%e6?TVFMdHyUo}IbR zI1279wdZvo*rueUgk(84VMgInb=6o8ExW^-*L^&|Kpcul#K3(|N?{kI>+5T}F^4<= zNfd8dAU-}mdTst9d!Eg|=~%lC&I<-?q}z{N;HsQPc^1zK>VHNoKcusg)#WPQgXMZr zehzo3BY%}ffUFpbkHS?+`fxYy{H6N@6YqZk!0)h?)TZ{SX530_UeNthJh3CBsjtS^ zmD7Ij<+pt{M)Q);slAbad(KpuKZ=cuYd*W~>guYp8>9c@HYyJMJ~cq~_F}(V8c#fY zvP}1OB#KaFVA521>Y>j=ZJcBSyW%u@1FaX>s6x^$|v>h&DK^2++JWGc*G_q_K6<0gbi^u zKS8Rp=*8NbtHU;^AW36-*>!cicHBqtX8E@=x2%cDBC5Eq{MNZIhiMz7Wq)EXv93`_ zg<*q{f${;^Ivv+5QGS0z2{k9~PquZT!UetgXnnB6Hc z{{6XJrtpR5op;&bN_v52$x#oXn%R$ZRu(RFFV`HvqzT=wgxE&3Cw zT$&f!Mqi(8jWzCmZC-W}=DNCuk!OwBh07I&m{ew**XJaG8Iu?#)y`s;jFDY#6t%8yFsz0eHEyy9*&EjsQU*RXAog z^q#YZdb6iT#^~)J03zKZ zblmK7uiWJ1V2wgKIwq!#a$cd`rFKaW5on63RbGcCf7jQqsZA?%7}(jdwY9ZbxVh1h zGDmDa7Z-Qi@k@Qtq~qko`S9U`i!db%2ge!3s48nOm5B_Xlwh0EQ-rZixySno3 z?CjuEQzss;^@D_pA|fIp?0W{^JI*pK8?*{FsF1U_XM4uSXWlS>XX2oxMT7`rlUCnO zn_6>lU@UBGCvm!9;e33A0s;ao?CheIjRwjuU-p9K;ggU=wzP-=$L+6d`+6+`60o|V zVcd%_H1nh7PGR>gd|YZV_yE3d!gvf@)FZSyayG?KjS9JPlO+1Aig`=A6NaA!UWbb) zNZc*%yX&2FVLg)lMjR&8q6RHup5F%IdDy85k&b69lQ5eXxC9+%XbTDo<^tMhX7r9X zvff~U55=gH4lNqyd(+e;SV4$eHRSPHM#9U=2uCGu%NuVGnq^*Ca^@^eG zW``YA0d!hy@4VL%gu+K`2u&(=y<1I|?kI7iGDX~q)XOGOf}@j?NDtc|+Dyg3!B~L6 zsG0sZz>pN(*C$u~dYlPDL$j1Wx-I!|om6E|5mU7QCp6QpKdEl49r?-kUg0H`8CW&3>-81B?ICE~V$ks!z5d%nZC7;tGkHZ#NG$I&p)Y2tq@ z+kUtJlDMp_Y=qwTw_kwsq&PVwmVN##bD^jFo&y44KCn`fi%YfT?qa^6x?1Dn=5lYA zp)Y}g*)OU1Wfl_%>z-6bWq=f7boLGo5%c>b0Q3vqEVaen$uTqa#JyIGp&xQ2fhlUX zj4Xe#H8rrP=~eD9&ZQBMdhhsDZQtcxiyc3Cwap0m%G>WfOZOK-05s=3+6M*{j=S+B zF*1f2-@bj@L*?~{?rYNy+i^qt-OGuI35=wEhWq==cI>du<>paP5k~Y{K-SmSyVl>v z5VPwt37t7Vh~qHQTr;Szk7!XZe$X0uC3oi3JwxL3NyHFrq(v8@${!?uePo+BMG<3 zUflHu+gylT;h0xm`@`K~mwjC<@F6__{4b3Bk9$|g^rpomhz#Xstrm6LSlj?1BUF!kq zi3=>q1(;PZe>LG3ci}e^=ClQ_iwU6>F7UNZ97Wsdb!{gSN63@>EC%} z5HWxq5CqYixqhk_62_jh)&)&XbC)6&#?3l^d^U4dXKnL{N#bN1jksKazhYJBup>nS zP>hwQ_K^XorwP8OcHh=SL}Lt~!ZGRTM0AR&2vf)c*5nQD)ms^K=WrZn`cDdxWedK2 z(?U$vjU#kLT`3Y-TFO>!KgkB6pkSY$qMY_x3Sd5H+T}V1)H(L%#uwoNFNGGMx42PJ zQE_kDuI0e_9fL*1Jms^Ac3ta>vCZ|7)8im<{W}2RbySa+?EtTkOx(b#1$2MRn5!KHM(ddS>$S@>(1&wsx(!4EQ#DOydMj1Sl(}e$oUY z+5j5COG+TKv$Jhf+xrbmT;MQ!FZSnUD@|Ihc-6855gr~rH)p`>IKu<%i@3#j(aVDR zdYvaU5=K*x=^ST8*T}5E|_{j$|O2JKS$(ddU5du(gvua#m+=Y zK~0V3#MD%53WNO3{ZzK2KzqZ8xOXb_+IRXrr{Tt>!E~i5HwQ^vCd{MsmmUAfVUT}| zf|62dYZOus$WDylEAQL=hJev=N@?I7dab^7rZwxw-aQDM-Zl2ST8WaCmJZXacl@x| z+5)f>VVJ9br=!-bA>;{iEg)kfAV=(|<562!=!Vr0@Z8HqKee9zer&~5t2`m@ma{Wu zWh04L6s6L^$0gS)GLKbV{f9y~`uh4j7v#$(Ee`&C|7Bn2ecVfGefl-})sP4x_k+9* zXi|h619$!(kl9Uqw;7j0i*(8+g}(FvS`&u=b6w?EAc+BjhGJLxU8Cf$zceYIwxSX&4ynH0R-?WC=QT{mcBBYVud^|DFX{aP~hP5vrPXB$EtmS#l8Z+#>^c7ysjjQZ~%{&7}k5j^f;9 z8RHI9hymr{xo<%4QIQUBZp^HsoPhy3*cdJq>~F&(4_MCC^=2Ng>#EEXG9D!RIs+Z3 zQ=P#bR!>=3*~+;uA|=gNW74VX>Y~#{1kyMP+<+j6{N?-`UPQ64N4ibz zEZ|PqbdPIlYUb*?0YBv-djg0{)%<>-D3tCoI5xWQ=X!cGsA`XX{kx*_$=>)}@m)xv zY*MOnx_j;T+Xt2$)(w#VmVqJqR(s=k1OIueWD7(3lW6*LycT2X9Hws=dG;s&6v8!2 zc-*(tt$>8MQ1s~&lH~mr!=9%ofI{x=;o;$Mg9?dFeoFELG_VHT1P^9))rCEi7lI&VjVWgu6)PLugf1mD^(fzUQrHy6p~5TtWgWl9skM z>)R%OcXxiIt0FgG1DEecz|yvRPwf-G`)t8y0gxquvoZ%l*A)-|Qa*l+1>k@ehK?t3 z_&8q#xn_R95#$ygkR`w-e{kvR>wE82_y3xi!6+V&S!nj^-uJXT`8$Y#hxcaMO*rRg zZQBO$429H*mC+K0!4#;XUW+&H*Y}V0to}(cFcNGs4X>Ya`G-yrc#Z zd=6khJ?rqeFiC=qqK~oQ!V(;6_a{v{oytpNFP~OqzIc}Kj+6DF6&GP1=Rd-L7OZ4< zO9Q}W;pB`ksg(EhtY6xH$wVlZK#r=IdNi&knd5b+<|iX7dyRuED}6-S}`1x@aOd(PRU|A>By@|nVemLyviv>Jz#clE+|R; zVeS9cxdGk5`{V4DF)bw!v@vq%dJ3(y^-YR_uK_`{L*W^@wfrAzFoArh(Utf1HI5S}#q`b|#fCgt19D zI)Hc@z}vjwyyXr<9QI<|d+B1qto`yz(E!0rNvfy1zRNvIL~7CL9#6D~K=xyW#im7|&U`@k0q7Bj!7gPz` zfSQDlZzJgJ%B8qdVt#AHwank7c4{m#USFS4WphijCv<|f^ZQq8G!N11Lm8Hcg75oV zMu4N?pu&7!p<@Yh7AP3xeSE%GUTlI?-%~ej5(iGy}r*; zC%U!mod+u2W9RSdB=Izo2yYbtaOVnkB3(JX1}X_Tef{| zrV0AT3$!6ua9C?6L!eL-H40Sb8jXD(8g6zBXu3EbVCRU!-DQl^AM6FkW`bvnVNNlW9qdBiX$`=AXFlX z(hQ@wm^MGNV+@_YqXPZplFDo#%go`D18^7i`8T@Om8x(n(?`7@#ucf=3mvI|Qu8Fe8o> z#MjC@U6amH$%lB5-V1|3pKrSTK0?jp)`>glNDrluBXlAbyq6R>0qrKVY>eK2|VzSLXA$=Jfe z0zrMYvxvU=$Q?ySq8$U;9#>w@G3zfqI5@ZpnBe=7uLtM8y=szoP@Cb*6`{)i_+mgn zt6sH5grh3sr`JDOK?R!|aG^gd14rt4xLbNal*P{Ncb-c<86&5T6(+57LRsFYr_+E! ziXJpd17WCrIx{<)<3G|7zYhvfpd={^)`}tl8qo_@%$;@qXZJi7(80KB%l?~<_>Y24 zAK{=32BG$4>B$CSZGaaMsusTjr7NPGV$3jFZGK~9lzp`+`LHe2{2kODKr|r)V#8(D z8Y>KDz+uudBNXO4G`bC>3H&fLoYWE9cj@WS_guzoSuYmB?EnJb+IYXYzIs)xil|Fg z%PQMaNAyvj1BUQC{r!8yfT0athrnC`hbfGx?iIfZAo&vk?f`{C|KoudZJU4pMgt8= zl~Uxb3?ew&6fKaWSzi?aaS!YGeA@JOW7gimaHtWef3m8oiH;}p8<*aLsf=wRiw14| zrvAqfWISfUhj$;JS;qo7h7?3cU36UBtA9d+*xVuBuN4VwMO1dji}65@WAx0nK$W<7 znqkoF`QZ_d9Tf9ALFcc#g@%i*J@tvAii(Vp5hVu+Hqg_TkG<#iu7TG9eP9b*Jz+0^ zg^fLG;7A35AU7`@G%)2r6+tEq@3lCl@YE`s<(Ulve~03ES&bd=H1NU0<#t(cFBh{y*M zf>Z$nA1Q0=^{0}|wX;v6y2mzlf#@35zZO!FP{7GhSk4K@q-y1L+ju09VL_BMSt~9l^j!Dv^nA;23=ctOc9wO~wWgjbx>kgg8qMi_` zKSnw%!3NJaz-VykevLZHeU^Jvp%mdAhTP|w3sIgR1|7q5Br&Hv-;bJER0X|%EAEDE z4vrLf#VAr(!kqxTWtavcgeqY=#&Z^C3W}7c=8-pA3ntuY5D@`_7O^8*RKz1x<{)zb zgz?R=9PGL<4tlJ&>$uI1QcE(0kl{K7Oz=ozZ||1Jmi04>=T?uI7?~`*-M=AvGZ^qH z(d8rG0L%;^#f8h2xoAxwo|ftD-O1Wv(Mo+>2hH!>)`CIlu^_!VS(VO!*nN(QaP600 zNL&)pG%7dYZZ;VKZC03K^-Gh6cV<>8VjhxHjw!3d2G`S7KUUOP2Abx9^ zhaTdA%S}rm2h~Jzs8?`>z^-RsoO5MJrQm~SCM1{)KeHr2J7+9h^|JaOWdPCN4}6YF z3Oy*6KLFM{AckAU<=v*+RW;RtJgCeMG(z64Li*1r+=e>II3T3(`m+I))9;hD&AH4 z&&WLv4c!JVC`ikt6}^%-wo;^og}|o2K{>c`>|F&?CZg*Fc!yHdgwsr5IEkSKgpzpO zWqP8+p@G43&K>U2(jYZl_!Nc)nxZcP0|SL3$3>Xba^oE;TS;ndMx=nO zFe{8EJSG-Xf*-8ZJIWr6Xfpx@0+h?#RsPp+`T6--s5FZ$Zy_Idu+%JOX`CavEs(>5 znK+(I2i=IA+0_2dKPMBm7~{FfLZmm9`7%8D^>n7tRGXc(Hj-dI1{&6ldUvr`^AV}sh)Bf0{BLak)I zY3q!JFwCeEcp#Fr*xlJU(Aj$be0R$qdu*$XGM|+DA{CnH_XVYNyt#G9vVVD{lsFdS z-9=$*sKo3|H_d%8n8qAXbEPP$35GAl#cLo*^#f9jW1KwMUUj#nUobwNZ=d2Q@cx~^ zJMLtQ(C>F!(VtZ7O#-j2TTXtkgQ6@M8YBN%2Fvb#e*uV0hynrV`(NA@_PZ_$H)B+k z!#=}Wj{=NW6utfIIz2yOOkETfhSt~s)H0u~vCb55&}k+g47e7U@hTwcA0q!{=T$g% z&~^O#7uPUD6FyTbc`MF`*Tx-ItnotsEHZF-`)kKL?Yy`5JSUNAwW zhhha3KC^T^J-b(k;d?RJcc_jLf*|lf2@CIDQK#*yufE%w)uJY4s`Mi;$QWsGx|@hv z;gMk>(;g)(-csp7zZ;T_ zz;9ej6g>jGr<)jQh^P9F7KyBR+gfIVR0>wNJ1^l(XJ5dlS35ltT5%^#^Fs8zo4v9H zB`kmcV6i*myzy{peipbDVOe+BW(HNq$Aw-Ozkhxqpc}H5B$)^#W7LAv{);fAPrPCz zD8+>AuPjla+;M-K4Xf&CNZIUCE5K~Og=Wom8In&7L`gs=a}J9hmBaX5RCeLyk@ubG zAR!@PHRuom(u7Jcny`;9;6_$J`+2-sqOg@@Z(LKA=NnB99hj(}Ca7g3^7r;dP5u+j z@wPiYIcL+IW_6dhK96%v>uDn1B^=e4Dgg=rYEo{XLlDJ&#hk~ppR$V$vpW{YDB}); z>g5%uo7qXiq}8E@d^W=!fZopkn-^W8k~RNQm&H(k{pF-F|Dp+E_-?aV{ip^e=Ivq4 zmCf~#^S;xp<1p}2v^P<8Hc(wTVFMaX(%i=5z?jAvfeJiIKa_s93G~9|#n>udyI4%B z$zsXMl1d#qmL{Zx2SXq4MgAhoI2^9<;wpQvvH2alM3d}a&xv_ zIPlceQ4-Er+{uxptGrh8;;@Q^3?hKtgP6s?RM_X_uVC?^S5qr+Z>f-IIG^1g!C8-D z38Q=kIA0FSo1Wv}XiroU!b^KUN2Ef{!HmNGS1t{?eIplPbDEhja`H-Sl2<5uMRs~G zdBMX=7%dlte5n4TQ$Q7;qePj>Fzqh^b*A7uAcxmgHrYARss> zGlA-ciI+L?40pwa9M)v^g?zyzyNszdC?$7WD#$d~!N`y1{SPRKZzy! zEbXhu==-0a)u9u!Y&(vD#V(Itx^1|7jiAos6)Y1d?91C?U0Ia@e&poLU0GN6Jts75 zj-AHK>vbc(BOG4t_NhX) z$J(|}YOx(EoBHvUS!RdVNd^n+2YJ=HI_ex*7oGxI1y(@nFuc5>uz*WgBJS(*TR%Fa z-i#OKx2`swe*&|WK`_A*!tVzm3enFxwlC-_8*}&3)89zKUUe`V{vG|Ky`oF_%%yjR zf2)oSuDY-OIQU-;8i#RCRKRteZ4iD$-PhTgr|ZR=YQw5MEtuPIxSQFVKpc+xjwKP3 z?5A;tHr8|!nn_eq?8pB$Z&%J9d$X&FtFnBf*(BiDkJ2das!O{yo9b=-q&*}V=i#Sx z)VCwXKlfe1lf|rt*s2T1UQ9TMCDKZ!lRdZso!zDU8!{{bOZryE(#-zxy{(Dhfsd1@ zeX}cErV!=+W*)MYLzEb`G!sF%o?zSFYA8`s0-liYShk|V%_;Bi?1Bl;>2=X1%S1W2 zj-RQhkfkD>mwY!@_447HE$D!-?e~9wtbEJMD++PrQkAXijp2;?p)tMH^-E%mY*7ep zJd&|+Y@<(fH?eCZto~EK6??P-l5-0OVHjV5y0z=-Al}8YcbwdI_jx}rtpJK{ykgka zMxfg}K<>3)a4P;P9W};+yAEQfQ^9KYZaPC@VmB8D!D`xIm;cTi7?LTSdW1yd)U^*g z-V|ME1O6wOKY=)*Z#1auI{!|Gq-T7q{!C-HSvVj&=ZGWm$w4hGJn3~DIiJ6a5Qywd z9Mr+zP9utpktU#heDpgutR|AcZbw=~OD{<};z=(A$junB}`TLA9hPS*gZ7Ay`T ztusLJ?fsyxu^BwF9rB{AYZ*LyK(p*!4*EXrY5jS3%v~k?l_~oE2tn)j##x46d4Nbm$ zyA|Iza^l#sa$%FX;HmyrdrXj=@=qq*KKEuNlh8zlo}dxw{792Q#*|#wUg^&pN;aJe zX6;U$`5=qTnm(75=|Z)~#r+n0SABd-vd|k?BnoTS1+LyESg4DXh47KPHg>dB$G$I3LkI^l9z!`h`^b^qgz=Ss;)7B*1!h;E>W!z-iB!!ecUyo2e&RZuRK5szkS&)A%E3izJ5^9VIteAot}P zGWSN5$8(SDvzbex-Gk{3(m%^4;f*1S``K74zNWCxg+UP{o-n(}jaCaw;%l z>g#jafgR$Ie>0SPo~f#QBKnqn%w;!jfMN*LcC@e7>+)T!kzW*2EB9qN z#36FEy1@%L*_h?2n-L5O8MC3nC7oJRa25Y%ubmb5nEe`#B zMafkzS$Sc3PC>IgGez}les_gaBGI(%ir}QiqWaqGOToVUQV$QD5%Wu(ApcCL8|U*> z%Nktin_?N-%0}de+fHgd)jJojzd9C6G2~)z_hk{5 znOaXTXf#;tbrQ5c8cVvqa$Lh)GSC(*CY?6=7;+p&ShcUt2u>P+>;QXte`dKbnTNSv zbMs@L-t(G%gZ2g@ia;*umuuSxM|ly!FbUN5oZqAjm+#C02l)2&k@$GYtxX{s%^Tqm zH6h@>T90hpym{JX9lyTfRijJ|9yYOY6xRyL(4jmd9^N3Hmaf#c+^K@bQ;{XT6OFSv zbcolFqPcj#vb40(wbsp2b8WR(8ool;lQtqGHPhILGIo8gjKRZ!cL3LX@bBm4~saKq6XU5 zH~X_#fftO5>LoBiZ7IWQ0UG4`_!0Ok1};s!cUX=7?%XQFs_6^NOh)K+N};o#BMcY^ z?FE$V9qpF?W)J}mG$7D#BhZV?xmxu;gFzLwcP{)kSD3LH#RtC}TT`JPTNi-pH~jq3 z)3XPKYyU3kQsbT|{b?GRZ|yiDf0hc(_ey$i)8b=2LlZTs-!I30B&{9Db}{tZpY=uGqN-WL2g)kw-kMr*W!{>>orak@O)r z@@;CpQ-~8<3AvWWH=kJD_vo*>-+b0?@&e1bd=`t|5bu5W8ZlWqbwtSPl+Bjbh3ywF z3$X#`czS$~EiP2iZ;eKqtZT9Ttq?wi{$T}8fNO^eC!;^kt4%aC^oJaVyIQEFn=i=W z;RcL!bV`SQi&LZp>irkpc?|kz3 zW2JG&@QH|GZeyCWe60uhZl9t4;;kPlB^UREJ!Un;*T*RJaW=W2dIJmnB>dy5w#O>I zg#&eE$CUIsQZRe}mrlo%!(s3ZmU)cS(|NoglR6SX>CfCN1X3L)#w+YDyE_*QQ{~-x zBvuT(bJ3%M#AdS_D5~6q1;X)UGDg%P(`^#rk`L1^pZmyF3k<;z9X(e>41 ztiL3a=Fjb+&bebWcbdb|SmWlvKZtbZT#l?bnQ#<)7Ibm@e(WAR!}x2Q%;)H?l%==W-;OS1%1MH6T#erp-$B`J~;V zGg$*8VI<7%8#zlC3h_k0ld|LMK9pJg93p>^T|a=Bpbk7`9;V0(*=Bf^O!7v`SBea& zW97umSd~>`Aw7vrd0d}^B#Q!(8GutW_2DJ<|51L1#81^r&H1`xerbJWbFHqXP_we3 zB;UiYB{1NxNo6qp%R0)6C>$L}kr?EX6vRe-*7(f4e2&iRrMx3L49k)_;M$h%I+P?P zy!c2iEj^*O5(`O^!#_idwLP{!SlFpK=HlCt7XII}06NtIpa|V56u%lt56pydU}7_! zUl4B=M7i!G9PyYn zZa?bbqOXdqM@vaPuPLFf-6b!+#f^qbuav>dFMF*%Tt-ksy>Iez7v($74z7dBoLOe%_p5x1bU4c=Le zG9VwP_-3Aw18QgPk?l+iMY2glJ5E8HFkApdk#uy?(~k?(E0;#Lu`4Z>k3MNAE5!14 z!Cw_FZpc|zP~{3P)&i;9u=3ime+p&G)t0!`WuELabNKZ+42bmkU%%cd_ZqPPXR*9> z_4T_sP8bkt!Lonz>d;8>rguj2NXc6OW=DlXR=Y1iRv~~CZ28EhL*OlWJd`;`@~2Pd=JzOyu-w z7}C`!9s?3$0}X-$_4D&~?xQ5#KK+qJ`RQrIQ+$g>;-f2=Ph6xG&(mQ zc-A9RS_~xKg@aU@yr@B)xVZs-j5@Siu8(-diw};>K#hHNwW$U%4xl7>yvqKz+Uv^0 zWxLyqPj1MW)TScTo$XYKbq;lK_w3wAD6%)NnLQSc162E_obUOR_DO`i0{!@gAFx4# zUWLBkD*?Dd$@|lZFaE0lqPxVeVShclNJ!YTX0#5gp09h-715K*In;5Foxu57)F@H% zEoe{?uODf~Lmxxwmd)2>WsJ+&1jjS96s2hQdq_SL9Y*SEcIKyDmS>euDscS_`RjU} zTJa(jT8I#TdPqK=$l#a`10nMb4{0mLJ>rF_T?Q~82XIrYe*BmZ6WR|(u^ zUNosmcZmY|FCl{lF;^#H>M`TzX0j!PGi9=y2}_haHWg(re9{$Uj7PuV^|!5@x*XR( zJl}M5{0N=g@Awojlq_n8i**w(-ztJiok0DxUr1`QK(~xe47pBYyDdB(UN}=LyYR!T zEXBB?flZZ>euph#MNwGzue~?_%i0NLyV7+f$$HFlNb53x=E^%Q1&#&mchmdLrmjB} zyeDB11x4x|2vJ+3*6kg5{TiB2@5+Pqj4MJn!1p>obI$)v{6<{Mnc1Q=snf13g}5)H z=t)$D6R|N;pdZ{&Kn-`M+{2s=qM=K`Mx1D)Dk~EJu_!yC^$|}FCoN&F?;s#anki+d z>AFfaVOahh#Y)bHDtY0UQqV%a|NV^&cnvLFDIqWFr*s(WlrB#tfr%B>Jqe~Yx>dw6 zqatZ1d$mp+*As-DD@NtqYi#49iI$&by7JuWy1ahbHtxyqEDBvYKewO!kY*mEon%q7`pPTg(0~873eI?ss(5l?$cRZJb;J#wIl-%9 zn$ghw&(j!0Hu0xfMzbZ@($-yOs+Z4mz}vtc>?PWxB#1Go>Lsq9m$WA2Y(ekmB>aWT zwhESB^81vVBoX4yDV(eR`OwNxaY_|I^u9KvfyV`+^6M5TucAM7kU45(#OLRQgDFOUI!>5tQc8 zAf3_;N{f_qNOw20@!tFHTQhHFy*Dmht^?=W-_C#U-)~r*xRwzvCbZuOd1lh6x9Cz= zZW+l)?iKP#sIB78J3&46C%g*b@%@#4^TZ5#tI?7I8Z7-hGOHi&;W^%vdu}ToDMEw8 zuaLpAKH!IN%#iyATKyH+%~;j*OytM1~C? zB}CIxpDedBV&7|YBa}}3F+iFWs=M-Qv7F^ia~63r7FzA+yk0wGT3VKkr`0*7OGVwX zayl}Wi$7&${r7?ryvp-G4n!J9!uptVu;eKk);fb{&Caa8zVV3yAA)|@pbb-GJG`5s zrL9C`qR%Ono@|m!lFF8tL=**=g1ALP!6lxm?XLvb5|e!LgO*SsUSXF(jKXaiBGEV@ z$3PL>_^0xZ-?*`s7 zjbHCFbXb8%@+GL(SmxOD;nk1sA5T-&dpOlTV)902Yst6hDVLQodm4aWwHiK+7r0N? zS+(gnl=$@k$}*;_wb-hGkA~*JSf_HE@V1DtEi^+>~%Wg_~wp#O-WfX z{%&sW+U5@}TlsV${KqRj2zXF&&4}6{)Bz1~S^ChDf-cb^*1`ob%P0dg%=Z{W9RpM% zXjzQ6UN#@tiE8KFIhxoQ0%uR7djAOBz&-*Afo{LRsXi&?%ON>qs ztgsX~yWJAcs;QIB!K$PLgYE_mhh3+}YdzKqQ6>0Y@-{5_d_j6Yjg8k`ps9q7&Uffb zD*3>N)qLp1i3YjP)(@DEUFGl#?AD6>@ANzTTccz zqf|bBn9+1L>m@NuGc{4^u(@bVD2u2dv){TD8d}yxhCU4D{OsFl@T_KQ+-(Fn{$%CPG(8ah`TsHFRRy2@i+PwPKqO zi-(o2e1`Zhr`XB$z`OnDVoqcuD?t5S!(?RVuqejll60S*BSPhnIFw*FH7BVAxp6{`dSxG(Q z)mR?fv!772`H(cZ&c3^YAzkKp{e{C~A%k>a3=U$4MG%iD#S6b1PJ=LP1V}0g31icm zuo=5Ss+Hj5ecH$DXwTr}sn5Ul$p z62B2dh~dkzGg};piStqDt)3KG!u_SnUFOp-3xDROVEFXjI6A7Z`VM&d3tHfb6xdc}LH3*ZFJxwXg zUd|oQ3dmEbKOFWu3ML^ESe5RD_B3Cmy)twGxYvq%S{*a!*44VB27Emof%AJ&k`qmOv z?|etBL`Wxj2Gd;{X&D{eI5IqXikkr9Fn#NXVpCBrWRf+ta~GohvVL%KV42`%rxux< zpO+XQ$G{l3HpDR16$%ay>9B}TQ^Z2!@Q`)h2CV{A0Fy~}4?X=b-#8qH9LtjzHj1z1 z3?ylChQs!Hc{G@nSAGmsoTi@_24o3}b)C!n!*Eg^PS(2bz<$m!Ye7Z9Poisesr z;!@XF$6FJ>+XvbG%%w4v#nQ4cI#G(tB*U05-&lW_Ttg@VT-#uB5yp{M6+gu+ML${& z_arplk#KolLhpP~&qU8KN)eTLBbs(?;~GM9-jBtLe30w2)w5(Et+SJL|3HzGcX^IB zUK4jMKX_ktUoT#GX~3@eO*i_-LOt21y(^T=?Wx-hI)}+|tSgcB^;8I59zo46)_hH$w6&x$GQ{drAYJl?QLpTifDa)R4RdIhA+oc>@&AY1dLT)hl(;i zZfGRi>j{N%SSU<%{>R&^R>9bXE!lz4Bdd=|Fuq&4CpnvZMp_km`IVBD8q>|d%{{~5 zlQr=&--rSeqa)ln5{1zAI<>^W^$+OX z=&Sy>zT+Es1sow``6V%+qv`+Vbsbn1a5ZX8@GDpkc_>?im*{oscB zz01s%U}SEh4DY?Wz`j{Nx0gEhq;{QaO*d4zF|l_%K|7H{*9w9E$@-I~ns!*;D@c=& zjzLOXKM?g`QlcZ0wPIvRsM)I8V#yl@m2TtU_?Mx4BM)$5}^*8SCs^67MSe_s=dqhK>6HI^}yu8n(_LL8&8WDemq!O;LvadZHf+c#7 zH7mE)U2bkN>lOr)dW4wd=NsGl0Y@D%j={Tzu_(L^eCN{|5-R35m>g0kA#VKVlP9l! z%v5&yP@oM;<`%ERXG(1H=o;Oo_};Z}%>`Ql-^-ipi%TB+)LA#AMK2*W=6Z|Q?*gP! z=m?8=V)X5CMhj9z+4;=4KWJOAmg*V6boePKCRS{V0$;GFKgpe}77H`%z6swH^k`yZ z<%^XCmOC`gRv;%qH5id(%1s*4KS9LyWr~r$J!26?$J*jUR|)^RvYX}hp!g9&TAF;w zCw9M)OgJoK_V?1TjpF`VVTbI9{=o~i>SYIJYNxFLt3T+5n%TFz0&r6L)m!{-W>;^Bxt6h=?|I$H-P}m&stB30E|)nr?uh5^%=9l z_X182C5WKC1>yXxfzYaHtJml=!QCv;UCN9Y@iOs_s-KLeQxV#;Zk@>d{&MMJ@%TVk;E+KyLegC)2GXHw)ba>9h$P}cKBlXz~Essc5jgC z=BeRm8xGe8Kl8HdS3drr>19}bzWlN3=z4C!PUGP7XMEf@d;A;0{Xybore)fKoWOCh zYczg5>lFl}T^bzxT>s7vt7kpc0MckFSW(7=`~yj>_wB^zteN7FvOcaTW0qvc8BE1@ zeY9|gPmpx)ea^exiA$PZR6Tjm%*2AGi4bhJ@z8uhlPPXw(C+IeIx+;3^iLVeFC~x& zmHWvL-H#TUDl)R48+VnPYm3C=ZXX1ELn z>BeQ0*cS|w&gwoF50*BrZXZiZ47~dQD8d!3GKCanpufoUthq_eq)Zp~dCd$FJ5Fg*bXI5LF1Q(K4r;!(STzX{r{dia4# zl95?!%Y7`D)09VH@2efoLPQj5@O9&G^I_)W^|u0ahIt;0eS+<$XseLkw(X73>a!KP zH~c&pOpk}&t$D6gnHIbl#qHVo!(wo{6N-1SUjeKS7!3X?;f|4>UVCV;{imnr7e2A* z6liJ|2Rm69_(ya$$y5mKSr1EjShNwYT4^G1vT5b-W5}ajh*}9Z0oQyBf3!K{0N4>DXKyw z^{~=QP8xx)&3exXsZ7BKYS+R0mKe#0sUUF8Nr-E}GCU?VM2|)^#5l2uZZ~4{|JU;dZJeky{_I{K!qlc(npj0 z+bzvGY=Rg(eh1XSaL9+w#Qut{#am6i_USq9^ z9cl3TTrpvZotkxd_c(Dh5Fa9k0M}6}I(&E1^yL!J4HnOLdh#IrE;4Gx3^4G{%#xGE zg6f+8;Xl_d4DHwu7<^zO74SppwtM-f=@Qr5_Q-UKJH?1f6~ve37upz;PtbLwBu<(v zpBT#IeRgeEFk^cgBdv0e^|e2dZvUgAv5Jz+_T2|Y?>iwEin2!#f&~{IZbCMc9k(0~ zy(na4oEYu}% zO}=an+1{E(26!aBNGggL!2SKTeJZYEN2#7S+!eJi>Oa*aQvVZ6ddlV?9V&NJ%@Y6J zjEQ-&h1|zbUjf~Ty$L71M-IAIN@J~*E`;%5i*|1jjcDoftbSnfA+8;u#Q^~_F8vy`|Wx1OO%mxY$F{!2& zquUEN5eM;xq9TZF`)~XC$Zt=HsNet5Wt(v*Wx`dV*Xg=BylP^T|W(j9(nELb) zCn+g*s!iPS@>12*RBDe7LVnlDbC-z>X^6?&ouP(+a0ljfO)w7Q{c*;Pq@sd^0@#Vm zGCwH0o)XZOmKUtY^yyVTX&4Rz-wv~R;l%zXSQk@);C*)1wE zj<_RUaA!ahOKD*74rkGVANIlJ>qarshk)874-=(-sdXHwIDz(q5H0Xu_KQVs^~6)7 z5=+8}P7#-$f2u?4xy5V7{sOyY2&;N;TGR}UpvdoQI9OWLCQ-}3%r43qF;}qy9h38v zrAJJ_sC<(1CnK$*t)akE$p2dufckAHDScu{C@p!K6zPG>gjG3Gsgy%paeWC zpBtF?HgMnN?*8gzE^b-JaT?Pm*V%^T*SPlelI!2n#xz$8pJd{Ex!c{Rz1?vxKkmhD z*D@NqgyVLUo8wh7I2>Bx^~VKxdZ;TH=_(L}0DoGtB7w$JW_w2h$TG?OJ<3t%_h}Rd zwbj(-6E>?x9$YzjS-ED|vz>2ZvR0VNjfi)31Y6)=TQt+h>$1onDz8e0%Hgx$oi&}Ln0g2Yq~E&)hi>D#a$0DZ8r0@pT7 zLFL;0Q*vvVvenHp<*O(97Px{;ilsRAB<<%P5t12;suL65tzb@K`-Zhyo^5Oqsb2mm zzMW{WVw(<9uABW`c3x~Zhgz_3!ezqEy=ZG2GW?hH$kWRibZbkPw9W=g%&se^D z$q{cFb(ep3PK3!W>Y3?qcC5pnfN)3Rc+kclF6dX%2knWgM*Po!!VSUwUw?GmL>1=K zl3iY9QcZge6Kfp5ZV(x(}l!tc;BQd#B3>&chw97dVyL2L9iUhhI)t4Z}AY zw&NW4@T&LbB8Qmhqe}m)7NCgvnNlk3*MbUdC|rv?8QiNWf5ppXMV@*oX8yDTfL?%8{WWs(%Y^@%2y%uUH+>OSrz5o z%`?T+MVGt|JO=Hx9X zQr&Ue@mTbA(b7VQ2rOJ^BGLsb{j0hVI$w#bW#1IKwyR%p5Y8}qbBRUnvGpVFyE?Fh zQZR>3Ty|~_I~^#*9zh%|%rXy>x8s0vFPito@JbSYlRJ=#{L%!k$mn{eFBHL+->ggz z87&|blL{q_D7+7P_sMCPH)x@?_9;+G2DLVM^xk#JKYt!f+xrD!#@^!L54{Z_0a#sd z=OkjoiGr!7e8l_PWjScWAeCSbbkSrAZd}DcPs7eZ#f?*&GmOE<|9n*pv296QWCdFf zlDQ52(}MZ1V{Kl~m%mj+3pf(L|3OuaST^||A?!HcGa;~r1B*|nsUKA)vIgxPXT85Z z!~MWAVAO)hG-k2bwPy&OZ|?uNz!*O8ld75kO*Ukb?y!G-p?5gL6DfHZY5Mr@w|ut1 z@8XS-$>*jK#YoKlbYx;(p=|UQ)Ye&}=4pfh60tdO5EXd{{R*$Vi*=M@v^p?)@E9=l z|0@KZm7^jNuP$}&Ar%prE|;kb7|i9 zxK!^hpt?)nmS>0?|1wjmzb!tft%2`MgY3~B<(X+X;{dpa)(VsR&k)aX9i3FbZ$1eW z+QfuBG1nPBjtJrH%ndKjAaX_4U=wFj{i#hnL_~j8&3{h3KU_GIv9?WC&D8k}4;;a$ z1iC$htwB8`f&OGudqscZyzhYG+z1K0dKw;z!kN z#E%%nl2r?GKJ5A9K-)?9HDdU$)XLg$oO}0Y`E+5-ul++oWmQNe-fq||kA$l#Ij@&8 z+S0$k0RAtGhUtyCSkx|Ngjy|NY8C zy{!IDXTZ}73yTgV)cX+msO ze!l5Mr+5Gz8z!pnDHU|S=F@RC46ZfYL^VZigD6!YIm#zrh$ISv&xPWhg|=GqE_D)* zd{LFv)IRZ`*47ggSVTXApLb)T6Uc_B+TIy=A-K_yun^Zmr~cCvuuYkm4gi@#vcpt{ z|LsV=#MSGHm`JP6-;Mi{2F5hPKe6cE_b5G6PN8hl-6GxMw~^2pKg7MI1c4?g3HirN z1Zdq08gOPSYUnFJmBesq?byT9>vv-+wT8-tD@%$$1Fa%Smero)Ryw$r`Jvj8SrgZ8 zB>ca{_|yD0A2Hp2((&HZRd4s3U68o?KMe6x+c1+e72-%ygWV4DFtht&hOVx_V;rT$ zslPe)zR{_;^!8u@Dt3 z;U>i-RzLRND(@&G;+>Gz#tvm8O*ck~Z%6#~uGifC{0HCnP)9$AKTeW@uN<8*E__Gh(TdV>!`5e7UJ4ImsH(p`Lq|#&yO4f@? ztPYP05JfCL9tz~$v^wPV@d*REoi<0v6fc?QIZbI5uIJ|&HLZG_WGjINR&<_!{yW-s zRDau2A&~nhvS|N882`^WgmW#c*=2tjsUad}aFPo?ZuI*n`h>Ka);k0$AgV=w^HUjg zfj|xzpL7o~`S#PX!NHYN5<^4~BSGs-)I~!;LqG_K&PXCL#7xUlZe+d*_)0d6hpS83 znaFgZ(g*gI@BJU`rsG4SGrO<9*F>Ls9Cb&JoV2~@Ym+s5MRJN7uT_ITGWAJx!ZEPv z)kRPuVphR{*E7oN74d_}h1SP|>jA%AB^3J7FS?bJfywn!y?~iu9BX_pcq=MQwC8cw ziNWtS&=eoO;)1lgH>L>cyp%0KpHLCRPXA(--A{`I{PX}E$fID@%mV%be$<>&$w^l< zG>OUAD6A+Y8+4BzL9z;P9^$US4=8=_)N%u&MIeA1G>CtBd{lbVU6&p z&df~;_RmA&z%KS4ou7st;#sNF59iinMnIbqF?>_KqH^IguM8bXHF43+mwm+Uw(*sh zN)Y-^R)#m(so1@JbdK_7dPPaXbizeGWTr6M9Bk5GJmmD=Wy8-{TdgD%whtG}3OB@9 zXAc>6zJ3rb!WiOis2=NxlQlE^t~Haj!NAZnxjGauOlb{OHWLYD`kerKZjn-iII zH@JvPUrEw?)cL@%nE+`Gj%W`<8JQ1H$!DI_Om$ z)APE%5}V8UydSqH6b60A@BZS~_PO*XA6E+?DR6lm1yD)U5){SMNREyx4*M5tdQo)n z@F7Pr_MNl4_o%YRa!q2iw2;1%Y0Sn+3Uc!enBq%~H3IM)C0ZW z?g{6;;4+oPpG7l~$olta<6|U;!r@RG{A6t8&naLo!f=W)h5}a&*wEii!3#c5X;V~c zE9=-WH<)NFDGc>mDz=1_P9?;(eg0z*T2&|3-zF=A`*~h7^bHV`P#`Axgq$-$5)sl# zC|k0M|5EySZ-#sZPE!es7^Xhq!tUG8LJmq0d7F}s?-@5JTxo;x>`Abmf{9fh%_wo~ zy(vw32;cXrrRZ0cJ0Xg^PN4TS>M8xwzIKWOn3vP|DN(kTewYluo`*gvof@)WojpPw zTB3!+4utU?%ziQ37#t$)d_7kY6!sMY*{4Sjok$SVp*`E1-db|*G41V`8A%YTdAsPwnvFr^WAwgJ^{mlN@gp>50l`tiLgrBv~@p5hA83gD^H|(ojRd%)1}>l zA#s*x4z696p{=U4@OeQOd8BvZ46?bu^D5&m_1Ks~q{SuBc2b;0U*|->^fF~i=E=|G zt6|)6#lyu-LkC^SU7Z*?t5jY+KyS|k-&G57u4QEQG{p9n6l3;`$4$o!8$TwYvu$e+Lu$ z?%1Vc9mbRQ26*T+V2^1Ybv?^6n~%PuX&2KuuV48!lB^C}O;kX;C=(g@HWQZI5rU$r z017~OvFOx)e++Q7Mp4fYy{UaLB2viZY3ux1Xtt!jlo76w6R0;qmBERY9?w6MJ zr_ZXjB^8Wfq&qu}Ww>*Pf>~X9ENM%#5fJ_1*KzZz5e+wc8JP)!1?|Y+YusoX?WUi**QInQP_$(&Kgp ztTKgXaL&H6c`RA+>Egc%19JLKlsO$^jA^9z3VpvxEAzvi&|5e&#>3>k4$j4-7Nx0r z)v!HX84l|)Vk3PX%EnzY@q0NgOJ;l~q$|<%-%CI0g9^^F7xjA?yq8jc_ckWnXu_;ZRSz4U;62PUI2DT#Vut9%NJF0inDrA<9)^ckj_)DYPf_DW z2cG0{18XDDHY#sQHANIdeTn-sug{G&&6xG84755PTq8?b(97$|h2`8S8D8{=SR4O8 z)+Ro{Hgf6Bb^4A%=ARX0vsSVAh5Zm^g2+@8{<{ktA7 zH|XEvWS)7D%PW2d9;-@}ILpYWQgtUF=5=n;>CjdUew)byx?Nw%+SHlnGBqJa5Ln-QC^ z?M*)oU{R4q52#I$s-`E(wE4vhjS<`^n%k)C%lVg7L7{sjQt?AhNAh{E&_mKp2ITF) zf%#{={MX0C#1Xw{rt5`3xMwtb2n^Q4_&Sb3k(qTNIPhl)Y`-APU&h)AVe4p()DVQB zq9sAqreef?!AmJ`%;4WNIWlgyzS)Z4$N;S!$y&(ohY*a@E;`6bJ< zD>f$CjVR@iCpX_V#LOz(BVi~wGouiB&#f1DXJO<|ODxC{YpzMBAfI(wHJzwuUp zfa4HDtLY+Kt+!k@aXdaQI_82Eiym*(9|-=@3O1YuB1HcuTs2@0Jo-?Q-hPnqaWn{ejBNn?I?%Y7-5SN`?2WWFe8XP+^~>L3Y<1p?6~_sW zUIw$!@HqA6X7S~6K65aiaAf3~{kTE_3tjuTi;DR?`;K~9n3Ni^Bq(pn!Dd!{1AX#} zU$45FDw}DmQ`^vR&c9a!FBP{mc%bO{{I5_O15&_nZE`>nie*pkEf_o)}VLc*X9L<4vLC zrsk1(7H#|z*1GEIh{|KMdKfde^a=Mr`*aGR-`jTG&K0YL z3M;-OBtccR%=j^dc$R8#gNrK#4UvlY)xGpt7?_wh^2gT=INkj8Vc}N8oim=QIm*r*P9DRk^7rtqc?+&`M5fsf_!YhrTNOQ5ajY=h1Q;< z=JWYZ00{UbW%eh)8IdS*Bd4k(NGHDkT(G+{9zi-f>l;N#6IP$-baQc7P*4DSClsXk z>Qy@c42@4qo7*2exNvDYSi)_(`oR@cL`T|aK}f&yVsc2v3y$;?(pmKYCI|>6?4hPV zK=Tmwj65dEvGd7A(*v>@O`LABxJGwj$MEjLzS z)ArMhnZPQ|mEqO1L3fz$Ks0FJq$2t8TF_?8Kcf=>%NhXKTL5qwUc|-^+9c`S+3l78 zDr|$$))kI_*p5n-Sx^9N6F@)kQ`|kXnsUtn_YVbD9F)mo#@%J=)oG6vBdUz8B+&_# z6gp=Q3*{AEI*fH_M($@AA=S0D%b{d;Z4Iv5a3@3ZS}K`%)0#8WXG&DH!8i}F`gcc3 ztq+U`LHu7Ji(@VqypNTWMvz*qTs4UFUNq2YvBxt-55Ec?E0w@SEMk7AK`@2roQ!fk zFb>!=6ZP%d!>D|TYm%(0ZmH45i)*RqjIYX(ArWwD4i0Pp>Bn1IzZ^kBHqdfngvo#l zapC7{{{Sa>|LHlIp~7#~&sk(WEoUIk4)Bvm053(*B-vc2JwWv6=>NIf>wx*fMma-_ z0u_rKCa;z+jTUfm;c>I^QoI8IqT60KC%06HMbXftdhyB&} z#==hgH9feAx4KT#`GCsOq1#{J*f{q<1JH6>V*kJf;j#C9-Qi?IjX#w)r~brcmBRpS zdK<~;>fHUp-5I^4q{IUtf;!xuPCfun_JXwUEpZpkgNl7)pu z-*9c#Uh1b$_!A~_pAzuW($WBeC>g+YdsjEZWjRD{;Mo22fuZQ*r%znoXEs>)s<4o{ zg@A}yF?5ttIx1fn{?otmWa90MKjAOU&FNz(UW7g{F#*J=Zt=TAY=HO){*Jw6`yI3! z=9((#^r-Ca*pN}s5_hQO_;n6J?8>!8X2po_lN|fui8yCK3fOvw0bptUrXtaaw6p-3 zf^proK%J&mDWTHsoS~+_NIuULwY~6YpgSokW+}7R1}JTr%z94>NZQPwm!+f6U^_SP zzrc6K!^>nw7ioyWPDwf34A=hRhw0=@TD;rqI%yaN5PVVmC@U?C>+1z~kV8J@y&vJl zw5yxhP-l#2rl;Ra<$f%>@PX(#{qlIH`9|f)4Fai9E!Em5GW?+Tj*jI_cbL&>5rGX8 zZmH1s0Tva;%n^1U@{KWtEVnx_IAoNRlmxeFxMy1?w5vUv+l7#@WZQ+llH_|3;L4`3 zU|=k2s+21D=FEl6F}&u4s&%m$Ht;?ywYIuC9w0raEGGo+O$@O{^rkb5e( z&10H_i}SlyxV5|)J9o?K|*s2>bWH}6t=R`mGSJ)sl~FP7gv+5 zF@b@BzSfOt+(3{;k?=Uo*w%&Qa>Aj%>fEhgTcLlnkuHssl0kW;s%4Ovf$9yqJct<$ zJ@I%_8r1_Y)W}~hetXNTj`C&y#kcZYMs(kUAgXfketUOCjID-QntqAYj8C=>S~9$G zPlcxC%crHhY9=Np+WU>zxZ402d&unUEP^{LvGx?=4TcM#lQPWKBUY=9hwzV2qWZC0 zyD!N%_SXl(pb|`OFNOvO{FPygKX$43_@Fy`{PoOqOmF|%I2$dw$$=tU24eOt}=-K<0^}a(%8@S)&s8uAl9|^3o$cx zjhJCr6ef$nV}Nawyj>!J(b98q;oe`B-A6q9a&vRDll1S4U7!<^^Rg-{@#%Q zQF>E|kc{E5vop6FfOXu<;d_Vyz&I5_agJuVc-|T?4Uljo=MD`|6p(|+xw{JhIvCjL_T&n%@Pffl^!3TSTlx|b!HYlM7=gDUeo`H(UF|= zk?;H2qg?zpkd1$0{cT0CZ<`0=^Sw*1+y7FnI)EWi z>xCP*IU*l90IK1kUxIPI+<(>wWqa+W6sM;|xD+gpJkO?tQ}xpOf_ zwdC9w%^tl`4P4@A2_J3k*7*|KVR2NQ56uBuxVrtd;$pF9E4@KPs^A=A-a*o<_m#foW7Eg z)Lf7R{sIFiZFwiveC0W1S?=9GCxzCuTY ztp0-0%^NWIA4y^2&yTBmkf(XvX?Te@X}HyVUU5%>0wa1Sg{%?mFj&i zTJAjht`hR_8u~{=w-X;!y$xx`)!xRY-JdhOPGkV6KEonGwM8p587bxq%~Tcd--CD# zFQ_7b4M|986As;7O-hI2ySH2*ZYd}{`4-V95@U0LgSz00-(u7m=G@|fbR^^>PuNio zO3y6bUdx)Dq_dX27p&Yv0-V%oQaa8&{P)9S2QDs;Vsl@A@xQE_?-70Z1KgQ~4aKqX z6ZW?_ixX&t@NnU--ycY(9uIoU+17c`7GIszsbrN*F6S0?1{}9si(*lI{5uuJ?%#** zv_8~*Dei-Fi7_k(sMf>8VXw0qo54MR&=bgT8(>ZmTn8uav{*uxpWS)J{KmcExuk~9+p>w^Xrc0nui zxqZ!>NEr-nOKEE_?_=NMw3M=EE7GAy2HCep*R@_7`kZ9kW2tyIf=nC<2(mCD{(7z~ zwImc7D6Yt)*6hq6=5xcf(O~z^Tqe5=Y$2bkl>CuhtKK^x4I8oVa40Y_<&tF0h#y`E z$oprp<<8pm^zMMV1@3Zg_~^rK8#@c?uhLkv=T3R6|zu7^h!Iyfgr#4t& z0pqwN;DRz^$H&MBhfNNuFj0Pf52a%-lKQ@CAe`QuZ_q)DOR*m79ow~#VAwVDt=gwS zS;5LgZ~{sQ%b_mW*t@8JNTp#doiKOb3kWGTMc@t}gZ0_z+m4R14-_%=9!SM>5m;ak z$l)M%D<*L}#1|q5fyjgWNOJX-6l=qEibGd7$r}o_6P%AplDH&tTf#ARe}d2N|Mni7 z={YOb%y8|ml`W>)-|z(EplHkUmgiM+H{E}t%F2b)t}=JNDrWDx#1?&GDHR9?BTIB< zgG(aM_%l<~Ame`FK5IEU_Q|&x=sVF*ODYLTJWyOrP>vMej)GypQ`$l*tC&dqhD6OT z8WnqaaDR4kzepN|4c-oFzhnc_+fGCBeIo%AG69=m1pUGtmS*D|5a3cdF=S!CmrYI)6z$w7N3=5h7QtI73T!x8zK6X|%JuIqxEiSMqKWW&`07I_@$r$& z^GT;2M&Xp?DE1&245b}Zx8Vb^+2p$~3f8D2m16NRpm5f*S-5xhv*4TKpS6 zc#n=w!3OWId5s#^?Rb-@xEffwvqXUdmhI<9F7{xj!&t*a^<|!qgXWDc|4c5r)gT4L zR;UsbV4<$vjYWU)UQvdJCkWms(rK3`^9IpTh*2y9HPo6 zp!=Ubs+&w=_;ttJpTDfv*O#`_u;uTvG&gN8sWZL;hA}k$YE*=B=Xz$LeD_`y`#c=K zJvwKls?uS1{|@yPGBS#eycq=;^z*!HABXWt0RMRPEbmfKHDKcl6SJvn&mLM9H+G$zHtIpQ`dj(F!QKt z_+ZITBxWcyG;Sss2NX*Yup32H*)>y4S6Pr~(vy^&deHOV@84%Guf*OuS6%VmUs|Sx z{9W*ayb6&8EcB${-i!=kmNjWp8U>?nBVnV_g>`F|tgKenluchsD}<-)nzo1It(|Hv z!TSeITeU{NiOz7loqenV)d1d&;=SOaNZ;#W|S7^dR*P_;rv$9*T8g*J> zU{M~eFQ4E4%TG&XJE%X zE4&;e`zsj-w`Gsxxo!ORbjde)GFbT8Fa^qtkY1X=1D>kaKVdNYJCq(o4;XDvm^6%? zg)s|0LU$gX<7A=-wcg6Pxl?!f;&7i8ISB>Ut2NO#2cPu?5&pn=V^NFkZ_)K)DPUF0%eWE)1!9T)xG8(YP|Q}8LzCL<0$FLZ}nvMs4)hk zw(#3T8E9G~?RrKp?SS+i4l|yjB~{D`UOSeonR$6qzGe6#J|QhAI5m|MuoDf=Ooxxx z`6AB`4ZZ7gdiMN~S^6P`jyuZK>@=AMWflz{AQ|KOyQ~=h!jDotvX{h=Fiq;V0T0jmg^BMFIIlGUpk zZ?|lV_1C|5XCg5`nWW9omnHEx3jR7NcqB1zU13mp)OlZguSF~V!tD_kiUY4vr)orS z2-L2IW;|N3U z#B0#pJWieV!>^pt8$P(yZ2;26j(|!#*;JD1ewFQzgo(OFynrP;FfF7HE6vH%>A7iu zs30Yzc@#WIIQxs61l0%X>-gw+bsEbkAC4B0?e9dYTMMe?TyQ%oZSeFp# zhng2T5#)Tz(d0vO`-dy;_=L%+k{vI>|BZwMRiG(PSO$_xtDh=9nz506y* zB=#`j*?K+ZrO*AXBh>nrSlc#NYI%To`&2l@-CkNN`VQIjiP3i@d)V3EkQeR< zz9=j;$g*dmpa+-sPhy^b=Rd{q?~xlR(qc}qID&j*Wmjh+fG`fO9{&8iIKLb=Tgsy8 zH}+Rh@Q3EX2dIxofe2<))W~jw%jtQTm&H`}N12@C_uHrsP9Xm;2Y-;slP6=c0U0`2 hbv)?O|MYPb`>H&(X%L-O0RsNKl6@^xD)lb#{{kY3MT`Id literal 0 HcmV?d00001 diff --git a/vendor/container-interop/container-interop/docs/images/priority.png b/vendor/container-interop/container-interop/docs/images/priority.png new file mode 100644 index 0000000000000000000000000000000000000000..5760dc71b557fea7ec5bd5699adfd1c228b9cdec GIT binary patch literal 22949 zcmce;WmuHm7d|?4Dcuqh(vlL=9g2jcbP58J(%mt@5K2ggfHczGk`mG--Q5jm&-**^ z{r}<2C3?v`&$HKFd&Pa+ zvXi8OIy!iHp__$)|6|z8e{h08u)E+t2;W$-pFtqcAqvtG>Taof3+`To=l5bqQ-cPt z`Ca8D*`L!;EQ>x(oX^zEA}pWqm_Vv#y!W;iDXOumwvw@$s0p|%d5&lhMteyaLx&qp zPfx%0JjH1^dH4|ahy4See^8Jazq<%E?31~R%Z5m0%4N$YPsVj5eGu+*T3Xudx3sP< z5NYswP@Mb6D(tw1?&#Xo4^LH&oYM%Bot+(%ni?4y`6QA` zgd8<6YoV5|y2rf5`}V?kz5m97xOUuQD7e`;cCGwIb zkC?+ClElExKbR0q+1cIncVzp?`ao=K>}d9Y4+&eIA_IZXG1PErfTPunMg|PEAZubm z`_K2$JCckCRcAj~F0R3d%XvaWYq_YbB>*>|`@aW0i=(Fjqp)fgx0M@rvQJA+?6E>g zI9zJwD`Mbla3GED-_apC@6bcpJ223=^y?k58t{(8Wvt2nMv28moSB)aJ8ZdC(bSyW zb2R84*D~}D7Q0)<`QL2j2qc%{l||d394%pmg@p`>$6Gw+)jFd^S{#HVB(B;qz*gxN z_UF$bi@uK{vQgyg6TbbchGk7Bm~Y;^5qu>8%%C;2sMa~|B2a$iUO&9R-L1(Cw{1fq zE=Z$D(|BVrd1U*`!Sz7`bIlvz9XwuTbpIc36G2FHn6@;Uv(V7^m%N7$L-Je!zvb@a ztzt5Fw@6a{69HIXt>-f`e%Z(m<~ z1QDxI<&2mzcF);9auLlgJ&CP$<3s6X^5?+2yOMCfoFp0+O!@-$=_vahm z$HT|)h6X{0y=ku4m>Bz+S`rEhHY%ShV^sr#gY(z>bG6EPAaW#&CR}bDJ`J3Oirq=5 zYiU7OdtlJ7U%x8BLJ1g^hM1X|73wVAlVQDTW@hyNyf5_?(*%hM3r#@!O*gu7oOy^a zy!#&Hu=;zsr1oFO)@V+9g1&MJzuoVIm%8QFQ|#m8<8_gLqzo-*dJ-h5#wjEa^xW@ys0C=5$z*HZwOjzCKwe74s2xbaI-jDw*UYeFFE! z??|#h>(YAExth;`g@uVtEhW0OCZ`*JrfcmCE0v2Gv!k`NwcQS=n?25V&UT^+7!8xn zJ`i)X-u)`qDWMe;GmIqXePj8G96?+@j-FltA6Pi(YEzrG!UcFY?X~{+iEoG?(IqXfB*jFH0#Bcj|;lLy`NEX;bOaL$6|Qx&jG!&Z#nE>}a_Y5194q7x&(5 z^}udjgJ^zL)vHudZ*mO{4Rj0)2~AC6P=J3`Ryy2WIl#^vFL;@l(8;4v!^6Ynm6b!g zO^#=$j*je~kZ~i6cwPj}*!&(!6H--IUn44$&5`i%5M*IxbpW;GZN#n@xQCR>1UVou zFg7Ix3LNf!Zs+tlr>>5aAsz$FyT=a!3AB^I&QP4%pVd(X0}Ain?Vt7k7e!R=Yk0;L z+Fie+p2@hmxj`36h=^Vo_})8%UkcysYqJp_uf;1zEG-$q)-6C9f}+?}IV{TRbDFZ~ zcJ`;Rtc=8U-XRnn8=DwMySpt(hU64Zt<$;O!*_Jp^;Tsc+@NA-GV93+R7<+uU6jL=cJggp87Q3(FB#)vRP)^}{_cbvQ>ick- zv6Ak2a4cBE(R37WD-uT^K4swfZhUHrzrL*WK^H6rp8URrb`S z?QGSj?BZg@V*N%_soW3rec+%}Req{YSk zZbf)3h6s6id9$mk)Tj4dDy$}0DlA87j~{Liv5F>GtgNiMTo=6=*@&TTt5N)2*)q_s zL_;5D8@?0)kFx@KMa461EobLyCKeWamqz_!!&U<}ohtcx-^crv!DJp#tB?Cx%iIXR z$mXcR-<-DLtFT)XbpVSQ0icVL@Wa*WLDJ(f%r^{_luOTa_YE?z3LK($M^aq2^8)bt zaD3E>0|iD*-P0Yi;J03_U|qtlyTS>&rEo;(F|n|M7i>w&nlHY)T`h-puXICY^z>3= z=oNy3g2I4rWxYpT_dSK{1X1-cTB!m+e>sA z51KCxj`u2u;e`}uOMF$h61z3m;A0I!6 zUSZ{*=OGbzt?bt>5>FZWHrJi>vvwc)Tneeae@_e6wQf5vWq#MGTSZlsrsZab6_1E$ zookqAs&>(b)#rZK5NeU(tqFP=R^tM@ZT&hi?9g6bKJ01*$`LeSMU>%n5}w3m8bT`M zQhe1T_CNz)QQ!vYR-gO%L;`&jU1Q^{@);YGfrOVPeQ_^Vek2;;S5#D_JzVyO$8#vM zIW?pU@DduCgg)$OehpD}!)YU~!-S5{XSPG2bCkLA510F|u6j5{3E z!g@m|BDIL@FgYssE`3~N6qIo1-ARm;KMcUvR}*DE>kkqD>k{5x9+GGlP2@QDYbY{D z(5AbsV8Dofz!wWds_KUi%-lRY3u0fv`qzRiArNxe!v*Pf7UsS){u7@6Ns0_ypqSGr z#Gzwi;)7mH%n+~egi>%k2PMbBkV=c|d_B?7qbd?^z`GQ_@Yz`%F3@z#Og(o?Y9m1Z zIzH~9hK7ZqUwV~jvKn}7zW_+4oBHueE^BI6#?rE2y23QHG3L{!(MFILs`~mbT@L2A zM`3T`=()`M2?4%fS7eAM74@pS<<;U^7py5I7Ep+rFEIu6>}YoC=`+4Ccy|StmNjCS znwUs~=+tVhFnqk3%FfG^ZMxn_IoPi=DJUqwHT1o&*5djBvJ-@&h)xfc+Y*ARt}b|x z_Ob&Th~~CH#JvgPb1h*>t@VV?aOv=f^=(%W56irQK{5 z#@j@8XEDyomKLhr$r6A++-~=4k3q23-~IwV2OSK{uW}g5=J{}KJM#-=SmZLbCw{Qj zb|HdJF8aBE04eA(p=<+GTpaoh;|sNYM5lT0Qs5W~mP8~Y;q5Jgh=j!T=`TI(-qO1D zE$Hv=ryCMQ6Dv{t|BmXgMwac3{(R>JO&1P}5D^hGC#=AF&?w*oEUR1Pou(9OUDgbr z7Dw|LK0w*?hXDS zzsmdfD-)Yry|Hv42g0JGFK*Ib^$@`;noubq1VgOsz_NO$P09Q*IOIF2V+EEi0V3cx zV_S1D=SO>&6|J; zD;XuF=qa4sKOZ~6Jz*FG4A1H4kjSHc=MXr0dpB#L?Ex+a8W^Zg?sxVWG%6YzC^X1m z`p*^OdTw`icA6&5`BGvN6GN9fL+M_>Ci}(a*}XTriPddWpm|!U=Hipum zfIpa=?@Rz6z7*8_g8}dbz%5leFAwTBsRmQ{hfP}v@bPg;NyB=3<$@W5{01`^lW)e~0KVuWpXBL2q2w=<+ooe#WMpJYs3n@a?76ciBCo~R z72+~SE$2&0larIT#`0lPx1dc`7`NSDr;-nTK`ZQr$T13cvCnb0fbZ5&~-#_ZQ=y5f&@7H-kDkpjHu26CU%sLk&!J46p9y4$23uym)h-60p_qrPzEaPXUq$ z?p$umRfxyM$N!uCdD^B1>%a-H9(o0<^Ud+^D{^u4@a9~1{QFsj`{_q`U;!ThNcS6% zniPuk<@dbNZ#6V7xmv&#qpiM=46x%)9I-uzD+kNHoZrrFs}Zj{xn)WWLUU zq}fYNR~HJ<9_bTQvyUG=UQ!bh2I0}kipSCK{cFCB)3wu~6&9Y~#eb)*O?q>&k41wV zO)Z8kqpAXKTUlAb`Yr5rW#9X2k6nkHK=L`j-TVt*2qsJQp#ZN(l5!!EN9D;lq_;MM z8V^U+pzZ-ickuZYZ@qzTwH49eM;DYO{00L64+ zvWV;@fEKzz_g}IXVD~hnpRza*{2fQHe?&^4x?7$^Yh?!Np?0f}2)#lFXy=^XH(y}X zz8?~}?EVSE!9Eh|6Tb!99?+FhWmL=Ej!l4QH&g1lIgN9cDOPL}aqks2%YM`Kw;a%Fbm! zgEBb}kA|KKORrC3HhhNV&cAJIb3maMOv8rvM(s)4T2m1GDugJl*MICra^!HzE_cs8 zV2GBt7yEG2AxmP9*Em2wT31!+`xOZaoC%l|DYqFqNHWbbgQqEfLKg22T>ry&kHbC} zJWr^ENdf6U@NQ9eX{7vENe0FOC57sC=f+MUPWVEuY|3;f^>xWk@4wk3xb*;b%>$@p zu&Ac>j2?Zb#6Vm`0EL7=87lwy zhn${)!Q^1R9x!OPo9%o!j&>dBd}i0=kAg*P)D?y|4df6wxP(7ytv9yrW~T@Na=GZ+ z>PdeADilyZ@BqW?F4QPeF))ZOE@lHL7OlIx8}yYa6fE4vh3GUPR})Z5+;)EEbAH)U z0lNe z#hNXtU(pKU^dzrz^?61B?4~Swo$ERBb}DSLy1II$ddh$R6zp?SJ4R7a130ju$_Xqg zWN|tC2N!FWc}xFReHxPny;Zx~>RV5eq@UB^kDD70IB@oOa)u#(cYo+hLJXCG>xc*b zl=`cR-uLHI2czZsb7jTFgqQP9FtDx$x1`k69RJ)_pTh8ZG&G1jFZPZ}lPT;)?GCOt zGcHN1bu~0V#leQp?=ne2EnkTUAVRN)>rDeX`<|X2C}3)uAUXj_1jG~%E^*v8kB*MQ zS21yFYaZ~%V(kh^peVRUIRJ%`2Hrm|(U(BMSRY7)KHT4WMs5P#j}d4-8(fGDKpq0n z_yDi}S($JDS7x9gNCHkeYdXjPp{1jf0_sv2;B6j}92>JkaH0Ab+$gKSlvK1K6ShV2cipv7oxhfZ7w*8^0T; z2%{3grKF4kxRj8HNJ8~1gDyM0f~34WIuMdjfE3cv(Shaik&is8T${~cSUR2_!`Rpu zIP189tzgs=mz;d3CFLmu9vLvv8+6?o8xZtZM%6liPoZjPIDkOH!q#7kc+Ja8q*HPc z!1Xo`Ik4T&pxJ{4=ovuRVPa)H)v2k!cPxR6Do|eVtadg`P{z2oE1w*P1ld9-PrHnBLE=> z7u3v{dS!ja#T&YRSD8e6Vz)`6R*FCo9=3JjNTjys2UkhQQQ7js8qohuDoKyOcH^gx z&46pHr7bsBAS#^NWN5RM+Cp{Sf?gLh7I00|%Gz2;DA%RuPU4IEm(aV>BwR#4YGO5@LUaMOCm7T~ zZUlV&j}}9z?We6pu8Pcc$`}aP(%-5=T9Vwg$cr1H(^)~L~1^x2=_xmhh2aG0Lqa&gdJR<`$kFW zcYcOX1o$EMXl?-`5P=bf)`zciet5N*?*Ja$0o0Cpi+JH8`W<0Obdc2KaJ-?&pk@*3 zq&i3~-O{;uBKP*{&Rn$F07k^BX#|vn82asOg(Go;7uahl_9u!jci6DUKkNy7_**U1 zf>$jQBM~@Gl@()-aUb6iLrNK>>YamEFr{~N_zvA45T@0_qSB7-v3w!Ul?2m==M7B_ zjt`bkGdS|dGgSF5+tnv$k0xt-D(>aUcG&cH1=&$P$-eH}?(Iga^y{wLt$ zIc*F|fWm<$=jH(HOv_W;dZxR!`DaPT;5o^1KsY#|E?JJ;>8O%I0w{9h=yAk?(rVO zlb#<(eOY*=%@?yD;UjlQ%a1E0cz=WLty*LibnI4x=4o`i(hWLA8t%+%_)UG(Cvv*i z>I35k3ytuGTPAJkRG@vALCM5cc9`26#?+hHu2vf@{$8#YaftRvOv{KkKrEe8U z22b0~)8b(TGW|uxmb4_auV2@94zneTh223p z)?pVb1ggNLuU@zQnw)Hp#Z`Tqykm2Ku&ejov+iiU3(UjU>jiusTiX-!?W;M|kdh~7 z(Ks?yToBFBh&!HYrm~AgMjL+OXCK?fjWL)6)SxA=%`I2(81g|EUw}C6Qul=M4G=>5Nyk|c@A0FqTHTYc7K((7cG}*O>5#eYC3eP; zlLVk897_0n(odOm-BKrVRMJ?rHzq26Cv4J|C>^wAKT!20@);>s)ShC<=J(tWim``A z4@WNvxILE&W5We>O*Xl|a204Fhg?K=jnf<#@CY4E-mKamANi$&1Fo5l@>ykilar`sGGnWfX|oyefr9mL1w%x?*We#In0*z$FjC2T#(l zlXJeWpGt{*`I-VXEs~(STEHn*_n&Db>RC8WuoCO)7nx?Kss)A%F&aVi^`~?45g3y1 z6*)2ASgUFuxRWIL1ufJQT`l+Uz@w>8YGs;KMl}4o^YTPa$(YCyvq*U3C*rhY9btUv znRuo_V2n5=lv%9Dq`xP!FDzJzjxN8PTyFRlsVQbe{H1Ni#FRT(p=zjJC`-AClc?xy z5}Ieu$F+8ET8GCwMX#0|+#d{Bw%;ew`~{!VD+EBwt*k}=8TcZWBIF*nG*JISO>OKt z&(AQIgs7Feg?olDA2d@sK_w%-2Q~LNx7I+vXP2f|^Ie#-(nhSB9~; zD;~{WJRBb*Oj8V9poF!IUlYcku_n02Gd1k>xO z4-{^u3&+I1#L{WOa3A$_qeEQKhKKHeQ>ba*wt-B6@9eo-=-x3!K5vdhE2EBV5sB($ zE$K{bj8OStEwz{~duOJI@53jG^6N>{?SjH3B}W{+L;}6yv+sVug%@cBU6ZNY^s;Ei z7X{LHW;Wc8%2P(CG@F%wTK;*G%UJAkd79Bi`s;XA){?GIXln0cV)pMMKvq#N+K#H2 zY=rGmgN3VDTdPGYi~@ACOEcMDi)HHI~HFG3tXSNEi6Y{cntl z=X4|I$0n$DXUkiWpZN+0_BS~FnLs;v7xMyx|J`=f@~7D8bhVJv+YfxM)}EG`#MHKf zJv}KW%a-ygABzI6qf9FKyM&F6*#3HknCL2zC6o&89mIV1Y#>KXLsbjuGFEIQEnd)A zbI_xcI=}LHB)i)j$SHs2@H(*9kki-gWc>~gV3Ct0x87et)YxA$hjvn2Y2OcAcV(un z<;D&>4_3`i5IYZy%C`|AnLK}sulbqvNU-zuS3AjrZF1xt-d_1v9?g#`5=5G|BJ0N^ zeR~8%_Dn1$kwc=`FEupmF6D#R^ZR!)F%e8-sD90#sLEic6?RzYV$Fx+LN3w-uo`Jo zE4EWB`r=NDH3Nd2XS%5@)(Y((mYvg$g5s%LUak7~bftEQ4e4zmyCmvZ>>kg zn4gH`1k$`cBAp+y(rQkRRbS+s&QfAE{A*dIn&ok(j4h|<200Ua>LH5);=I>!Hw533 zg>Q;XZ*>~+oLoAT?D;_3M2?4vlCYod3=+MKEI zXe)I_+gl0|W87MYcea>bp+rX%04~bPzg#pt*yJCqwzPjr($zkiB9ixP2+}3eWBhc; z0!W17O;NExl7%EzV=V49lMDi2T#IKOvRTi6mMIiy1(L7 zup)vI9+Q9@(f$)pySf@O37uchDnP`Rc|P)7qRhJV&<`7;qGo3|L3tk+q#1SYJ8aMAP#zE7qx%{ELu>F0ercM2e_B7}_qM^oU01uRX-5@>6TKx4HgCEAf zarss;2z+sS0A;)bq-YSAN+|zSMq3E=uv>L%?4wBrGj-*@Ju{&OK;;5AFM{>IxI^Db zn5R*Cltg|_!Y!Lh8xF)oc^;#yH)d-gv6(?1f^5ktz>DZ2)2+KmB_-ibi7c4s$2CW& zu098rpey9r{G5m{?aoe;m`)kpi5x5pGT&;1n;nI4wG`@Ayr8-^nh3q42AS_S8y6~vU&&cm*NNdA3@H%jo6mAOTeqY&R{hO#P3&H7hFNGWAX z(8%*cRIQS|JX{f)Xx-AW!D75YX+ z_fQ-Z?a1CRROIrzlzBdwd3FJ!9JDYdmNZ1yA~#D}lBpx`9KPtm>mMmZ%{#jMMUGOe z(Tw(^)gXS%QUo#oc+P$?NVU1^o(ByIob7+=TC}4Ke=3%cS==A61bGQo$aH>3qHAE2ZJ=KWP*RBG@Xx7zL+`!1#98&*|cB-Bm{hCdJvUPuTOi1 z882nue?f;}NO{N^i4(jGxjh+L8GDC93kt6@ms*BPJ$Rhf}pi& zBZ%Kbsgbt}hUmZ4&`@80# z`Ll3HdPhaQjL$}c2)AP?@if$& z8(Gs)-g^jy5o&z|gW;?U&3I>}PI*?=lj`87I2zbEYoaK7m*em&9t1K=t&|&?6gGB( zZJRw|odd3b3d`w`zX3ebfl7f6_olI=lxw3Hd~DjV;)e=|UjeR2%Zw+vORyB1%BIP~ zQQ931!U*4&^u!m>McO^xFd*fx2`#I+DJZhL?G%3g{7K8fQKl9M--CJo{(aNIWYX!? zjZ#D}zx>lj0;*yu%NT6p6adSMZgJ{=t|OgsgD8?rp#Q3+t7=-qKQ~O%o9X|jl+T@y zupluD^VRh$7X?IEE5EKIx^b>bsh|2tx)cNCRapL2eCw~W+RKpGbGUvtd!lJ`}#8K z>N-bpHsU(x(;#(AtQfu9C7uxVY0qoQG_lz?ue|JSl244Ymwj2R_X~=f-3S`}oX(bi zTd0uy6$noowy}PQe2bnrZiccXA{qfv(LWS-_Acr1dP@+#E6JtjXk%}%BcP$VxX1l+ zCS=M3MW`a)*Z83>XnN$~I!B23%>tfK=6?t7{{K*a{%_qWbAJy4j+Un5rcoj|%fyY( z;Hd~11I23mt1QY_OA~{fdg+(r1#WTl@}-9aP?pvnpi^ymNeaAuLBnY-Pm%ZJo%qKx z`6EWyY-f8@<+M{1A}imf6dqe)10sBcC|<(%k{X=Omcl+l{P8k`)kXe4V@Z?9F#eX5db_L_x%fOsW$tFeM*jW#Wje~yGMU6t6V-;M+++Sm_HML2&Ehe57P^R1Xbcd3;7HcKPP#9Mnjd+Z@;<} z3;1nx^u3G)Ge{QYdFrB=i}dANn`EHaN!MYreDl`cIw6q!#I!U+yFLup?CM|pj-;Xx z`OX(0p}%6uy-GXP0*YZ6x%HJX!JTmq_UMs_>G8o7{L(boOSN%{Y0O#P|8ouSnpa;M zITDG7hS%dlU+pxEodoXf1;5-WIIQ-y)w@4hBgn{llbX*gc4NFsgTo52OyFVZZ^Ec7vDX#e>wUer<*;tnHJpU~4K|lC?Keg3VD7v+RB6`OPBEG|68|2Uiq4561%+}pawX1Y}{gm@! zADa4tqs_KZ`tFP@rb>s<>xPTX;!TrDMT%lM|4l8!E5be}pNOquHd5N0rGk>~nP$(J z$I;MoJ`dl$Z}DljM?0)xedMs9d4Kq?%{Hjf&*K5g`G&|egWfL2j;RTJ$3jG!xZK`*q$K`AO z$Ns(%WYm?B(_utxv|^%l^xY|Ht`DpBJMplE-HDJ;I-i>_+5`QW+JiAjh`5ujV(KYR z^N_?0s?HTi$iiuS-Jkf25U3_MlMw0FJm-8(CQ_Oj*=yWcamazw2&KOioqxndG%;1J za6d>CGn2NJ z$I!MQ)Zmqk>WckKbrChcqs@HEHFa#(z zFC_9i)5Y%bHaaB56Ji}Mo5PwNxg+nn?Ol4X2}=xuD?MF|yPPZemGhUUJyUstik0JRTMa+@EVv9iP?TE75akhtYAgEU^#j&|c@C z!d(>laQtfMU;zo;KfBtR^Jp07xzFsEuU?JV8`+Y>byV)XWQn*wqv6e3dgvbA!ENdm zfL^n&Mxm_vOVQ!tIdfvPRXpAgFN7-mEDPOUf(>0i-j{i{=orEZ1H~jKce7yVXb|mz z0oChOALJJdY0v(N$Kj^%QWY*lN9Z@ER=(mkOCye$z;uYE6(_)t2+Qg;ddVs?lkkU1 zMhg55st}z`oA_2h;KF@6J*3CrwyZJ+5uzc}eV`u$LB0sHyRL|#Pxab-2@8+`_a9;X zRtqUdK+1ZG{mDU$?VUX*ZRnH0>o0hP(Q9>6%oHx_HglLWM&kDG8QUESlD1&x%R z_IoH0^+Heh^?uW_5JG`z7Py>^63YGx`KFvxol+R`BkRZR{Kc+uNrAY1*}svXppJhd zu5Zy1CN{{5Oe(-(k`HCTdKHZ%-VK@g*em!Ce?go%JPSzBOyU;2F}Nt{E!i3 z6e$ua)DI+qS-#%*lBtbFDi%)yb4SIWIhG0#6h_>P<=>ZvIgVP-X#S0bH=Aq0DK~|u z;9()vO%$r8ziA)iVYpnDoK@{K0;ip*m*yPZR{qU-H8I-?3oPcD|&XX)++)1D5SYRC}N@UIk{j;V}Z*vmdk_lxs`B168MIf z*lIzE%ymtPeBA~k`oLCb(lNnOK~eB_&;5b<*Bg)VwGCR2J7no7(%_qbfR9)x-Fi3^ z9|PHG5l5^RT&od=V#l0~jaAr)nX3PgnC{of}b+_#xw!= z1b9g!l^VC=U%23S;Y7+A<+?z`9p4xU6;U$tEc7XE)WKIIzd|WA)uU}j`E-72DeA{( z{k5NE{v4b7$wI?uq;N$!34$|9x@Po+RLdPvZXS;xW84?-dmpdpl7MORMI>>Xe?rUW zVjqv?N*bJoWPX+fE|^JPwJCPJOF>w+Z~JltVe8IM@rISpITX^Q`}^MToQPeMZgi*( zx%u4iv@MIbGg5n=&otmJm*?28V?McRz2!V=xs81sMJo_-K>TF@EBCoK-kZXaGt}An z5QATh3x{CX^n@t5h>{w!e2X8Y`doKo|K;Npudf%lJY`*~h!KB4JIrY*$Nh;_CU;hM z97ZOF05K{nm28)z&bFU;vzg{LSG(N%_r;deK*^TVT-khyJa4^R>}~5~Hv6I3fC-B5 zhLDvtm{X{Hs}tVegE|5gjk~L<9wTOEQ9F_J#OJIoHew{hCADeLO>Oq*`%5w4)|G(c zSG5s30}&8LN6WP|+|Qh? zY(@6*lq+`==(jpLv#@w2K{Y+M37)E`v(zhj^>7c%TS zOj{oB8u|Zdx7&V1i8fpF`_*y85Vrqa)L;#?G!hPPSQW_MS%iv9iw6DR2Ak&p+f%oS zpwG(9M)fkn(r4#u%&C;hbm;(`rGQHTksbq|-e%sjsWK;v-DcpyYjc(8#m`EqEIv50 zT-&%)jVH$}eDtXyFticuon*Fh6~bOK?z?@qtU}(R&sK0p4tECc3R|w3{4Z3Ol(t1f z!%)V@M)HVG)T}~3T-?%=H|2xb#8#I3Ujb$yr+g)d^eV(?3sLfUU+FV)ZIx?Q`1IN1 zki`S&`(k)?EUP@wHT(+;W1b@vI+pr@kEWH-D8xX(!{cPp6s-Mh^+OtpvQ7t~%I zO0y5E2Sq&t-Vfn$haq7==*ePuW2yxq*;(J-;Al7taGWmdalO!1RK-$w+WmFh9^=F0 zPO8i|*-e|0I5UH_Q`7%gOgz2(uP043Nu{ zmz;8EuvN8jUc&gP3DxBX)8{KhV6CLB*y7ydZGjNQ0MOJUTQE}^NP3*})u zI`H1cyV*o7h_PfYYW0j*Ssx!>p(dALG|uJE1Brc`Kam260+*+UeLZmV1a*t1cx=Vl z4jNdHJklbkT17)RTuJmpEE^f4@nylsAUkbSVnhBp(162LfgUnT+jFww`JWbsFY6F0 zwC~3J(VzZp&m3EtB>Mz3VuQS}MsR=u9P1FZQL*Z(#~VlJl0{x&G>zA}^C^-2{h8-4 z4gZ(hc*vGB`IB7&=VaOgAGpDI*X8jRRw55C5wr!kh?d)Lh?{-eyVRvac0|}!-$JlA zQf9gO58%)pFBa!^DKx%ZNF8>3aW44OHYM3-^zwHQ%F-;CLV71B1TLc^xqsWIvUcr; zA06opO=J7nt+nY)oCr}QC55(#aLgZWPi}wW+!FTP`HW3u=j$4PBV{EAU(YbFc|g89 z)U8jM{J@DueJ%4>x9M5lv|Kg5ky=ILT8&T@ZB5r1ll|!bSzRXxMar-py8ONou_9kh{YrAQ{t*xF0mwZ_4!+WURc^aU!f2298X8-;4a0 zDhLfs8{z;vs$ijfHpc#38%Vu)wb@C1Vqf#yW5#r6=zmWlOOfhkwT9_^sRs<;>ZA!l z?B_i^=s@c(mFv(eaNgNJ6d;t8`|lvnT7H6e+T5P?oKt@}V{JB4&UQ;x8l~Ux>gU|G zf!tHM#4Prx>#!lGUv;QOwP_u^rt!kRzy5ly&UPzUyBZ?7wNW3|t*%DyeJxFJ+U7}H zF!t(=b{ulwdGp>zc=20P(QN3I*%(G7H7WE6H&BSktEB{D!LrM4djCgnoD}-=6k}QO zoupBJjDRB9cfUb{Sc9AaeCNDCcZ-V4s}EicuW=%bXm@`l!M5=KJj;l1UsdC{bxDGz zYAfgOCv?u;Ms=3oWBHK8qY&B zP8J3_kZB6B(LZ2`3$*qul%HB8c6v#RM%MQ}(*+}!BImiZ?l!Nrrdk9nRDNn^{r?8# z-wZueG+H=>ia#j6dSh^R;PP}(ZVlF8$8N$kq1~A%{-u%w;hNv|WCdnB)d8Qg5PZ&? ztI5x=DSa#&**54-SQjpD`{7~te|~nW@L3os7rysF9TtLv!+xWQf~4i*6H>H~k_6y{ zcz3ldT?gt@N%@Sbi>Sb`JJeG=XPMa{Y`<9rDcTAR^)?-YjpO$ngCD{uxbUkPqX8>* z99`FCF`2eE`{yb~ABB3JuWWi4cq#Ssr>*0Mck)aj5Bto>7m9x6v*nlmwqHAV4bO^Pjt{NW<;yip&UuD>7J5 zWhR!T1X{ev27~h<5N06B$ao24KpZPvdo{z1Ov4k}{E7sneuQmsylh?NH9^ zUhs)?b$%U2NS9(#SXWV*gpmopdLqv!*b#QPi0%3TM!OK&>$L>Z2#~^&dm{x zI(45!*NAa%(<6dqlraP%_wMDMhDHnoJ>@B1JO=pFVe4ATD2hPyX?rAu96$iz+^cWTd4Nd{LOghb&K#J<<-Ar_Qd00>s_+A(KpC;9aS~!zj&;iN|wUzX! z1#6ZLTe=WEWUM`~JmDJ+11na(s4jw>1`$M@njo+jk509on7xK5Yy}0Si54*c7oQ%ZHfWx~>9xgUQeO-wfxIL5=;+$*b&vhWjjNOa_l(QC zjZ~h`&dzb48JnnheQ$C^b~G=%19Fvdv20zb%WRv(9M~6W{Ikomsw%O8HBLnwgn@8YdwxYA#ixzFU(U zz5N|fyQYSvem^*NjAS+d@_zkm^hh+CZ?37GEwm;2@v`;1`=&m5{nn{ql~405wx(Rq z);WvkG-tEZVzbq3GwV%iys%x{)83rYUb-`s^H<%%KCL0?PNlO}g%fqw7GCv~*xS9< zW&QCftdG+b$Lk@vBCS63_Ml5mvvdN4_md-8f|n0yW^;^}(_X|T0-wCBdggEuhDYwRjJU3x$^=_1E2fdk9vclI zdzu^FCNQ=;y4`Z@td#Uz*~iBwEqzPyg$yD>a5OdeqLlik;9yLu*RSmc4p8MKN~rPi4LJYR)jyaeLsR|@x_f7yyZl9t15g|mL6 zQZAf??5VX>)=En7cIC}Bn)dKTY>_dAcu#{?c`o#?% zrg{E-!m134>YR9KGp9LI5hx_2os}8gdQWT|94SThf)+MH^7SWS7c|OXA7+IknKST2~<9o+4R)j@a;PAz^ z#k6}wsu!0*I$`2`u=zwEw^HNwiWB^<3V&brrd+|}8T}~p8tqy%x_oHZmm1Yy7xb?B zqxNJYjjKwqq+f-Ojd|Xh<2cRP`7Td*_WL|#N2TwAs@H?n5&zQX$QVEK*>;z%r>`Rw z?SIYsQa3xDi;C`z&6PP1Rp|u;Z}15OZ!hmW;Y%N{Sa8fReo9i8v zU)@p2{ITXge*a_kja+WIIJ;D3TK8*K{*}YXZ`MsV-~j13pX&snfSo^THXeix<(Bk* zGvl1O3~!VBPs#$B!da_{g&dzU4@l;y^6+txkB>;W~J3(F|dNPD2@f5LyF6LgTay`?*eZw7~-q6vLjA#btzlq za$8BNoHc6M+U6`k^yKeX^A{LzGzLkgUQxi(n)T!^>x+qv_wvC_F+=gEOUaRrnYl+3)+xB$`|*@)Pfbw?ZeoqPpfu;6ZgBM8#;nW8xOMNe zZfod}xj^u&ThxOG*>PO2b56NgG=q{3o)x(FVONmJ*#-UbX5sFnLh6%!fusAM@}DVQ zrR7m61t~o(3Zdwz3y0Ie5)!mb z8cgZ5S`|Dcr@~{h{Ea;ohA9U($XC#hn8=-Lh9~of$huK`wDUcdRW_Xg+kPp=)fesB z@6!g}XzNw@snhff=zac*o0;wj!V&{NvtmG!f6_Ftwj()U3;WJ;w$h^ML&f1*;@ibQ z+Lfrfg^qG%s0RYZ_M+p=)Fo?V z-a|}4z-NE@Z9k%yt}dsu>ry#tir5^Jijc!R2HH-Vw zpwSXLvC*zJ&Bk*fqFfKzCeH>|WRk$Ewm;~0j+dn3I@8kD3w_sf?BfW)PIn11jN^iSYlo5;xggBHT(<2uY8RZC+vIM{OI#mwii@)stgB;{vUICe~S^^e)8pH)v|*4T?h zPQ?uy5n@gHVuhWD>X21@eNx?helyGtS&0YxRm;)Ciq3Z7tX-*+^cN>1E168T?emQf0jOYy{3_J3))GIzMxN}%xoJ5J6ZKo--Lx*LaGMbkZEBZ2nOb6n z+vO23gxZBM>68rV6oP4l9a%p4F;u=yq~?NId-F z(-zVSNVRJet2yN;q(1qLobfIre z;A1_kK7x%RMTu-9%DjJg=k7u!oCP2IYd6aQMVOD9Si7CyXW)HznTdmS>dZ)_w5~?V zf-Mis)q)7RlKWx!A=bVbjohZO$kg0uh1#_;kOk;bqY(@ts+yODUjG(HrGGspy=Ub? zeRI5pC^{&%eT?C}ygjqHKR*kvMe?^Z+2deRAPz>H=mir2N|Jyb7d;E_p{)T6yIymG z9TY%_G#KmE_|xISS3Yp~^LzkH&_dAvbfyG#?=Qh>k=mS_m)XMr@X8P|q$lR;Xs-^lZv35RD zEg+^lNKEf8ixt#(^aS;cdfqhtJy4(U9eS_5{<)OVQWq}Hp}M@W(K->G=OaDE9BVcb zFL>~k3%vjgE}f)17p#*1xoBC-2Kw^J77~dRziNj2Z`T{G^B1#vw(=!g0(vqeBU!!b z*4QPdnxK6@t&4H-(cz$#uhahBN(6`kvmQToGRkKv*3v*$N}EyuxiqPimogc7bZH=( zN%C{#`>29OTpvmNcOwAtF;tG$`|L$gtXu+XnCDE$isjn-GRd}0QN&XpVAn{TzPZlW z#_!${bND+Fh8`vY@~IYI9psFCPu4q-fSw6)VBM%EE!CH;W6ySdd0$*?VqsxHZoTyk zlF}Tt`^(1Ch;YdXh#NZj3k^^d+~%r5BZFV4oX|O|E5XH)v{wNMI4Ter#Cg~Vb|#>0 zCr45R51N(Gg}{LUB%CgG*z6(era#wPW7Uh0bSs$43+_z#ZMB$`zaozOJL)$+5B&1q zMqoDK@L6QAr$I`&n*~VW`FEp45QJ>e*{EMh-0}HNKA--+T2ZfUYO0S@AAajox(?i* zGII+HihQ~Ff0n}0$vAvO#EFAHFV*$27K!~mrfLc|=$>Cq2uq=yB!@nhphZ0e#bO~T zr@u;oUj<+t!+mSfU51)FgsGRPZ#!#~%OJ3rAHe?v;ECAZuf%+&6HG4$P29B88!qCd z7Wvju*UG*l94mc~m$`Q#SG{}hXxs6;o z&5BNfJ6BAcyNKwJ8P82;Sob=V*uP zX)WpxWNW2c*Ole#KAOx67RIW^LK|E&7$sl*vKd2Yq4#CBUq7F_qIXqc@ilM>ov5fN z^D_Hzkn8`Oy!aWcg9Wxq+8ld-l0=KVqW_8|0$5$ji^UZr_FFovNa=79;>AQ6sjV*{ zfJ69}xMjkAzD@hwZEo>lN3tRtMm3_IAmOo;oU=l#1#8O+B>4-Ai+z^gf1hIh)6Q40 z`-`tc4ux|_C6LKvDgdxt=DYLq*}tI7(x0Ev@PckooBay`TVP|eKh$3d((d7!D~H%O zG>DoR<8^hcq%{F(JC9aJY85(L<}>Vg*7pxc`zv+6z&w%^6U#TvX2}XZV#%sU{n9v~ zs>xScuf+<6s5sa^quxTM!1VO=D5~+43O9?+&d$BD?Z*7;-ZPH@DRHttj^R%`X1S{0 zXFe}!A%f5Su7QEUZ6@Je2E6Iz+13;gcmG1aT2ed1yYJt$Z^iqP;vHcp4u_joJnHN= zfREyVYeIqAvSxYk^6}j>Gkf1E7;L%NUuud%p=NBDBWMs~d!ZR54CLJVqXkqh>5RpR zAPJaj2*+U}DB&vK#rtmKjaF)zcYu`B*PW+V1z-%oXUzjd;4*)xbzwC@1^sQ(1fB|8gv_pQ&n%I@ZPJc!<|-4g^4-P1rFYg^rJrMu}GgIqtitYtFs2G|)o zJO_@7mCr9Na%X3hoI>8**QFya>Jj*Q8)7&K?i4*h{UTYu{jszIpw$Z`5WuCt5j0>fd!#mPB$zG){MAVq1F+B!Pq_)7^%NiIM+kd&1CeZGeQ z@+oUGOpvAl<~1KdP<2WH-y9uHH?V4NXM+uKST}eAp;1G%j}Dhj>LlWUIwT!RJKbubRPj|#vrgeU^%1+m$KGAFv4 z0X(!PEam-=R4@?e4(`!_)rRlJ43H3pY{32jm81^PUD=?3$&=uN_gaG_3dhOuvbU!P ze?&cy=!g4N;PIx*1%b3(16gO%3Te@2qZ94|W`Nw@K2L#rHprp`o;PQurltZq$gT|nOU0g!ZhdtX4{ThA=4SD2Gk;xCqhh6bxTzvXU_ z-9K7w8bVnBI+gbZd+ucH%O;QHwY+=beD9u^j*jA@790! zzA@f-&oLZ&&OUqZwLa_fnV&i5Ugw0XD9K=%^pXgJ*IGi?NBJRh#8CY6(SlM z+RvXq#gvq=dt)dgH;=iXUxsl+7c(S8O3~+Pg?8vfPkqeDJeu)#pL*VbZy&dtr8yGX`H`gg5{txO-1Y}kzEn^r@~kEh!o2(7KH zRR}i9uIxSHbgPWv9xm(KIV52*spoZA;CuP@8zMhHzoqb`$1sg~wazgZ4Q`g-C8-1f z_{;?fINS5~pq>MKlq9-*gv4!_#_eKKd;M%gGz`iv3%gEi4kP;S-@mKuW?w~7@PC`k zEUv+pH>xuJQ20E8HUbRB`w5=7sw&=L(;h1U5mB9Y7EjPbR|tl%SuRN~Y^=|p;QiUy zSOtUSe(~ak@57Q!+|AwD$VsWueFWUU3kczL-X68bZe;~fadUGU2Aq-7gE)^>8i{`X z{Mpqy9;}GuhnHMj1V#b(+`Unx>!fTOZ|2_~&4-4%(_hw3Ep_AZgCXq@i zrb}rscD!0eRe~~|YWNz9p^v}bogT)UI>P3yIxmSBNyPG6Qq^HU452Z3f#k$ zC?a>IW<;d>yY;xzm29~|b7HG1?B-iEbUL**gk@TLJ2aC*KK!t2XDl|k?TwC(I^9ed zK0e&5s;I0SHG%m;;WKj?XBwRInq_PT5?Sn({FGpqSdv8Cn<{rfDQ;;I%AA6YcsCII z*CTF?#s$2{jVUfG3&w-QPzr7y=|bTXXYG2Sc=U1c@gI77rG2j!J%kATiTrLadzAf8 z@hSM6AmG|T-K*Q5)>DTpFsVf-NJ&YnX6lUU7v1% zV-Kw1P{p%Nivp~2ab|?v((7 z{cYCcHFa@iWm0z-j=YV{u9T>uq-3zr&32yJ+qXMsA_sG|@)d12HXLNp{s9KVmba6Y z{?BS_Yv2F*iMKOfU%9-u+8dqbBd@GXOy8mIBWW2C8{3!W*cQGsUMMl`_R^@0LeS$d z7)(ys^II8OFQyBUjaO-<*e1QZKGHb2OJGO`Xde`;mR|1Dg7b2_%l8pTMPtVRq_H-!m zo;>-uF_2^irr!W=Rqb_VvE8(^b8kQQ9gUpFuA{=B*;gy`Io%`Eix=>ck~bH7TUQ!= z(R$f-B_;ilY;BD0%?4nV7{DO#f;wp+lBxu%1X8`R)NGC|C(=ph4T$SgcLKF7EH7Uw zOqJ_BcXM-lW&Jz<>~Kh3QygL1X8 zwZ$VRkCaPb=(OT89WPKc6z*yDzUVSfGC)=wukye5*3j1WIT*0xp=V~sc&?N#W?(?s z)!hw?=#Rd>_2#3t?l!;M;TwKsaI5XHJUnXZfT`3pb8~Z(zsIX81V{jarTzWeEFY1B z;1E!-B|)|I4-CLTR8>`BK&+!fqM<3}?Je-FrpBaxUR6T_F==3<_VN7OHHu1@Y;|?@ z=g5c*0P}RE!fC%yEb>sVvrTbTf`x#``IpuBf|#o-*Xil06G+X33}Y}rZTGBTofcWpj@`Qlbm!TI&; zSCjMYF;MMd{{ABF2Xh;ondR3c&z?O4VI~psWDmt8{Rk#)43J@KazJ{*9twSDVR0io z=DbeCIBym!%)pbT2uqLZ>gof3{nxKQ-ff9K;-w93ax3*&)ci_N7GVM)V|bMXN)gQ9 zC+CO|z3AihNNcYtHy77W1CPaL;P~nM@`Kjy?ygWkX<6Cl(u$`3QW?w5!4%VKoj)G! zw;2n5SBq-8y1U$lR{x8hW1O#+7G`~EPPn0z?%iC^ZZL090vaFvspDKHsNtKqL z|J3Vjh}UoT*T6smhgq+u@Rkgfm*no=o_TjD)~C;(CvvWKmLDJLxwyFg3T+i;`RnH9 z<+UD)n)PA=(EHr}a8ddGsP*cmr7qxhepx6Ml#`+VwYjyGmDNQfPwe#WcwubI!_A(W zki}2H zA?v*E?#)RSeY%lk6fwT@S@hu~)|4l0{-Pl{ndJIrud-*y^^Kun=7P^r>-+nQ=>h7; zzcMfkRUH2_67WzH92)9P#c4bBvbeM~;q>&qvp~i-(;oB`(SWv=`^$OL`uVRF6-pp} z^dJoky*3D8M|>}5E!Uj*{*2~Gd`e8*tgQqHz{JW*R5;EA7A_H#`hWzE@KaD>xz?#I z=fC9*O<2LwXjK}rV24S7Bzy+iL>7#)oc>*p^|5&XB!kYx#3Zxt?s_C3F*Fp}Yb(R! z-P(`v_d8#c`txE%UO2X07i?j)@zeO<>=b6Qo&HLAKHwN|s|T>}3Aim=``tGf)PFPX zgqM?(!;?~d`s$Th+ufhq+h7ZbZ)y_Y_x)RiNyhcl1-+WTjF6Bpyn8h+nw+;E(4dVFV#ByUf8MnR z7&ba%K%>PY$m_@TTwTBKFEpy_vnJhb%ZS=s%_QC)HY<~nk-Y2A)hd4y)=dlg(ZgQ*!@!w22{knvKwYBd z#luPFNs4?f#f0aXo?WHStpRCLGcaJ8oSgiWmbQ2OsRb~}{I)j3I)}vMM{O@P_stl%6fV>*v(XlgPTW@aCDV!oJT4s4&K!d}=20&-*v8n9` zUDnTVUhuF%0<@Hk)~lri&~ru(r1Mk_Jy(&e?d(23J{&y~)y%#G!2iu{PfHnU?YNQw z@_k}zs=v;DzNJ@@DS{4kpoISZH=rC}*-S8wET;x(oY~GVoE0!e6l&W6@Ddqu16)H* zTl+cq=vku;I}?+fnwpw#Z}=kU2(b4n+ips>_=dPOG&O$_8~Lu{{|i8tFMz%M)i7$J z(#aPfx@X=jj(`?{Vok&m`P)%SJYvPw5BM(I#l^+xN)V1We1J>bZ!X0p&vh)hH*eN- zSR59HXM`e&@bQef$N+nQmr_+RGFqWQRldf-67m7rP(Ul zMb7K+15i+m(#hRMdoWWz&tsU@VfQ>X28>MeELSR0$Sft**09}Q7@T*gSyKQ5N?1EB zP)J$35E}MK|2k&%1B-V7k~angFy6CgBbqc25-a*Z0w^+!3B8t(FySII;YkD39^~Ad zn{UFe;Ju1wY+71c&(#oO70@xf(xJuxzGvF}1OXvoci-0m)2-}t1au68nDu;ufljB@ zn;V=Rmzfz;RaNCKx(s^n=fuRDC=LNIW(sVSC;0e&4;kOTHdX*2Q#P=jDmy720A%BE zp^-gB!SkSYx~1a5rBVOy`MC1f%nTF_pW&}iQl3og+DRX^jJf$U$>4RHa9ykA=UtBO+b{-mvd3h)uz_a<;MU#8+iGLZy((g$A?^I|-&CLJT<>nJ=;qW?rhe^SQE1frHd+cU= zq;7A|;%XoxD+^UFvX2GurI5@H_x9~u7_)G9=K-NNW+(Aony>rUu{FUA6vtb3Bch@t zb#=)GEjTwvvSI<2E^TqgfHPjdh99)3nSKBYz0lIN!PES`r;A;Qec53 z5wXJ%K#1^jhqso0rMNLgzz21%?AQ+t4fO=VA^1{hYiI=5+D?0W%Y<()c-q7kA%%5+ z0A)?oRAII|Q4FHTw`B&4R90%YVfGHVisQXxKaP|grl z#%5}o@)nm~jRh8ia%NPuS>GB!F$oY~7zALFzRDjrdKg;mwWwK5)N|;}77qek@Sqk8 ztYWP2B?;g;s~`bgwv-z5Srt-VA;rhX9|(kYuk!7=#Kgviq*H0-324!~ZZ=<_!!hC! zMM!v^ryB!+Ds%YXc`Bsyoun7qm1!}*k(Y-_nuP`ZtlO}#Ff%|)0r?hm-`{qvRP_1L z+k2Cq7#a$= zJufeDNJz-cH}e50EH#)c#8(#ju)S?HVt=+dBw=B}5D^hk<+v=OJn}<^+7Aef5Tl3F zBtY+Dr~NmF(!xNldfB))>a(W}0bO+I&7zw*Y-eXDWLp_Ez9T6ifdEPXbjEczqr&lU zAT7nlxnT4~rqZz0m&%bRt+i_;kwp(Sct{7}v225x!t=}u#U&A-P++2pM8ua*pOsjv zLLUtzsL|aTv8=rO+1z6^?-Ep=wGpu|hVo2kYxuoDhLxRN@UM5TZEQ-WUF;v0BXT{3 zscUAV_+2(#twz0NYyk_4U2)=bf)V9ALHmlF)}V#(pV3()xtue0JVM7E2s&-P{4klM zEOLzqe6S9n&H#3>gJgO7>{(7r%L3=rCQuMK4sF*Kz{z<6%fjDZyYaoKWyeVL`buf(=2V_bw zutJW91&^}7eS4B*=*?8oxJd@-0A%$~iit2d7dRi?ITB%2fC6i1X_x=*-d|-!X)NVz@s3LYsZ^uj;2d3UdepUbW;@ukpNQg z!JyywAss^jTdtLr&LEg0qM_L{Q*Aa@s?DaM zq0!+`4~z$x&Csw@kOsr}GJ|HKqqbXCAWu2G&#m-bhPXiq!Q4DB#NkrQf(;kHweaWc zzJHxSJ+1W+8~w->_Nj7Smwsn%z6z*6ilO(;Gd>FJ1z73wI;4+GekA@4V(UDv$ZK-? z=ht5l++%=kt3W5i#toDR2M`B?S)$a`zQ>*Xed&0r_`~!)(RzD&zP>F&*MAZ+(OA+f zOX_sXPfrKf20-z)ZjSFPHrKc{ry(U&r;&CaQmN$cGTree7Uj7Bl*mH;!v0+QY z#>y%VQ0HFTC(4jh5FI%?p0ov*5#dZZ^0ck^o3%(bp&ET&>h`84-NV)^L*R{`c*V5( zTqVNzf-Qry!Sn`j6)sYpDV!>)ufUmJL%rpDO+asJ_C}L$rQlnGs(^`1fKaC?Zvp9C zI-S%u0=yU3U>YWESey4>jhNx%<8OdfY#u3_2P_p-3Ik|uA}%C7Jw3kw7#^m~HgDX1 zfA{MNsgP&Yw67u)6Vo@(6Vu9+Avc4h3_%ZX3o6IPN9*wdIe^|}`i;MBl^7tO5)$Zv z{{99KxAM9>HTDdq(H0%9grbG+L9?fu4^5yV2s zJJ24~R8?uI1MaFVsfwo@Y~M};)$@#vZNzqMYIE8XIJ=qrgSu_K)c${S{`~plX;=v) z$r~@PM&G-WzE8=?+gBq`c^y79Feye7W5Z%4Gb#e=zloU;I98d|*;jOt-VRZr| zkTtT(%6%X@D!{x^9LL4OgDV{0aVeX!;mc|Q-`oHHdim<6IS|7f_(;jZK0JVI$0sBNef~_q7_oNr4f?Sc zWJHcN88CmsD%&5(QVXt|^I5wugSPL^|6EGo;q6RU!Ytf-1NksTKqmm)aaxRIMh^}u z@c2p+Lwig+0o^(Vg2ohP>*|(2u`yA^kJJ`cXk`6Z)o~ z{r&yNpoCX}uqUU$4r?FaceE3QHYs`7it6HPh}!@s6b8FaJ%rj-a8GwlK~w5x3%HgC z1^0wCX<&K7m1G`=?h)fx?Lglcca309=v3D+ogO=i{J} zKCl)_%=qJ8YqQr=Z#^LpR%2^9h6gcn2$X`8S9zMn2(J?-KLp5 zO?s#2_@;L9!*rX#M-~)j6~{fpErakob60oxO~WbTAWleNiv- z8A2uOH(VI$bXq()V4lRxfxD>2i~#IH#AOFEGDz>@$bi!gY@i^ZptQ(4-$!C8x7MUB z9!eU}2>l4W*4{GiihDgGx+D6>pK-5g0`Vxs9xl}S-q*|~)I5?CChOk-KB612Pmq>=%JT+HSiTO zHXubv_}}L%({DTN=p0v;GnUvLtD8#_P?>h9dqhcls|AlFhk*wd`tN@|;{)bMRtd;Z zf}ilM_uFNGyYZ>~E)3F9BxAeFhPS(@N;L%O0ZN-N%t8;k+ zSVkFT*HLfLXMdbX=XcR<^*h)leY^}dJbaXZY)u-MBpLKJxIGljJG?>hhHxQut?Vbjm>cfh47G^M z?dZIuuTtng2q|S#>@F5S;h|)dQEbKUzUeWMeDqYHULwDUW^JjvLDjz#C;Cj14<84= za>-o7k?;7jWS7G8wA_HvYHX#YwGP5>J=WlECy*a``%P0y94Z$gu4?=${SmkMzR&h} zyHpTCBvdk#>^O`%R`RHU-M6WgM6=fY&Pq2RAW7;^(1FYfzu{6~dw?q>?JCK~yHeXu zB&NBdzjH9Ym4m6CSPQ%?5s2BrkOq+{GJ1o^fTb9UT4mMaUn!02bmd3ESkm($_ej^z zSD^uK)-M;2%2X1}BP_ypZ2S0thlf!t>xUCqoU!fR=ESrAz87E=*G3ICmDf=(Vm|Bm zu~Oo2mdZw;h5Xt+OjU}&l4@NDq$vmc{Z`kbiv zq3I&!7c0??xR6xVzxTF+CyOU!PbE`M#^261a6LyWDe+{*n1m}^@TI7%WD2b5Q5dI3 z{;|$`_P)9~n=HV~k&S}Y!!PMPwy)B$zvuaUq^i~;lv%6=hYc&TwPCFluqLssbW#@L8b_cOJ z$|y#LHZE;;F_*@Czip*CC9b3XFzufn32|!rJMdD6Ll%id6E5^JIs`n^Ht;N29MB6+ z(-b-ll4q|{=0YFXJDI&S)zG6PnR!mOOr8`&&US>MiL|7zzS8(jN|m4};v*@&8Xlp= zkBYp>8P{xh>+sbE8wIj9UJ-=r=X+HqSw7|8*xWNEPl}QC-K@!SyQBWTw?unkdr&3F zO;KqFfhbVSVdQ;#Qsv-CT1QzTX%K{e?=7>6qW;SHV1|q0Wl2x(ec`l-ZN=(fwe(QE zGWF8Dfu1-w&rzkAjVWIRRYK@dw0z&{ny%f9_UiRBasW9iib=*Qx?DVzMIDxcN?kh> zPF@adgH#z2D2^1NeTMf$UliMKQe&O3M-8EouhxxKS+KLpLVjh}HDxoXW}DsO=^w~E z8sf1tqQ7B@uHvZ^^;9)Bi9=_nWgr~GlV(I$(NJC94K^=N;SnNyTRiqt)Vk-fwHCuo zk-FbL>_zpJ4mVZf^%K}^wYI*j{b4$C*eYVDPxM!ZUQJV&nRd59k}gz^Gb?;|VY zcL}wq9Hi{_p6I<;Z@NH^wHi+UW#nvIG6)Uub4RVyQZm6s!lhi6Z71dec6yCEV+**j5^iV4I^x6etj4&{Xwgkvg7*Q(bHGDxOy@X-@7 zRp+p$VQwejVr!~qmr$m$X4lT81~k@|u?*UYnJS=a5H?sSP=kK3agqF2mJ_q&W04a+ zX2}{iPi}q_o|e*r>>^|y$MM6Qe-s67EI>twuE;WAG&TizLg74yhI{iD2uL)(TH}7j z4)R_td~kfLcgE%{r5cy=1WB8@Qo<{ma+y$fbl8MmQatQHX3J);|B}AcgMg~RA6Eex ziOugl>TQhPoF953JbsWyptH?K`e$xG-$ZvkOL5kz-r?KdYDkqBM`BgPCCcrvv#~7; zM1!bIuk4R%-bQ@H9$J1(V6I_j;zepkFFENa4s*`^?e}Hi_~FF!FG{z7)1ONMk0u_2 z1#Mz}1A$C`OSYKwGW!d!Z>_n9QGgXkNF>!$@5rrs9v9?5u{l>KCuOSLl~a^sNs*{y z;?c!GGXwVqG@XM{cr*Q~o}Wu&TF`{2WIK&u=gi~kMoATdc>Tq=2ys;nTiQ&lsOg>50Ya`|XAbD^c&BkhL7I?l=<4 zh_A~O-Ou$RTYDW;k3n}30}-hLcwCZe+f`QqnAS(f@VwG0_ z5+eGh(x4dd^7bCo->b*PQ+!A$wBys5xM3^5e7^(VtRB{xO+zd2qnT9-F12X@iv{@| znWjhUF4BIYXhH!74>cnSM4wlLQgyX0qg;AYxaf0{pxN!1Gja31Ec6wuDU30*Vjem2 z#h8=FxKPuN{0)!@ykJuMcC9b7KTu4$~ozcGThj$M87cmuo8h5$2{}}b|(B{#UMlV_3rEvF(q9ts$@=* zCP-MM?a8k?x-7W|XcK7I!;Q%YeJtv?^uioZ0zaD^Nq2xMOgtMu&6$x`TRjmgH!%OYFX)yJi|kSzU_ zXs~STTi@Cs3+*DYpLI@lwXbK?jZbd4i}wztC%*28+)p*J;yTA>@YOH}^c3L#l#7JT z3U-5;bTW}8HADl%N?&IZ69$A{OobnJwkU8A^f$Z5Nmp&%BNb$*2T}2<_LHNTbKf~_ zO6zSsZFX4itad@DGCz7Ha0&;hyBSm+sC=Cs{PEirvz)RWQ$hh*fg(NX`wyo|xJnK@ z3k8yNOo%L$YkeT&`RW8;H2gI&U@z991d!f>52q#Y?+R~}XyG8(NyvGsA(7)%h?G*2 z`M?VTdlJuWX_1hSU{3vrCaDXR(Lo`2c=G$4x*Cr`G9=B|WxJ@X)<@(=`J(7VS+2G| ziH4N8c{!z9^e|5!cW@ZViqmfS13Efq=_pcpMM(vtn3@7=mqZj}{miJb=>4iTL^6at zo&Y~$4w?9ctv#;u2*v*IA6q(LwmWkW5-fDJwQgJsp6t4@kT;Q7y3CccvoT|HE+X@e zrf}X1le_`VJ}+N=qWAU201lO^`uM|!+%TBhRP{Y-)TP9SqvR@1AjHZ`f#mCQ#T{7@}BxT^y-E;Wty3UX}f&}26Q9vC{0BJCSj ztZ|w1UIZ5x((I244i-Z1IoC~iiglXmAgS1ln5yF8K||rtk8+W?&zb-&8aeima;2)E zOoH2Hg>Gk3Nu#sELeVMhD(1SKigEALSH=4RDixfXZOaO+Xk6At7l0iXmzJby!ZvWZ zX(nibxRw{oM6`k{|C#XfNQ&|m$gC~%D_-oehd9wb*j zKza@gzTE7!hi`6SO#UbC7<4SJ36{r=yTPIn*rm3)h8Q(1e%$hV$BzAmf}Rli*96^= zVqVN3L|APxEbz!2*DMa5K%bLY)_L(IU2bNS_zPU8p|b2gw;Hg0l>wP4U_JHiBK-6a za2QkgW)`v6S4!$MHzl*Z9a1+(%jf56$8ONOxSENkZ|L3YfK|}Z9 zwiZa-6=@)qFxC8fxoeuT689`PUaQudveUfqp~0l37193ML`JUSv8{4^-a!@xB^4lu zPe}=cxLt>-_#BHJPbk9$iT78+M)0S`foPq;4d*0J+H5)=S#lB@mRHp{FJTH5o^+*GbQ zXf_=6IZNkPo8C?sFd-ulXiCkdPBLDfVtm(k7O$CIEvt6*%*tV6B6+wK%{CoppwRzb zB`?C;%AojqakZPT+LW{`42S+@EzbMHM}E^VgO3v_HBM{y9&FxN6MpQnNbr}kZfqbn zO~sqzUYGso6I248;tv_j5n#{6;;q(a*6V!=?-QvjBoW2F61JCOs{j_ZE2*W=J17{X z>3{TK1Qz*`IEx52iWM>ZXE)v{NZweL&26+u-Evovw z1f>+fY{U_b82$53!+sH7T>*1VI!f|T0>^AiqWwc@9dd;Lb2>M-%J1K_*eDc47+@@G zt|%E*OT|!1Ap(hq+W@@8RP??0Zj~r$zEWjci_u!yRCc`QO93M;`et@7)hq@BfzC-S zuU@=(VZo_pITV`9ShVmRZ+k5=K*aBG3Ztq0qZ)G^MOnx1S|kdt-$<}H6-j(zF#2%Z zmXfJ%)-)d8RloeP{Lj80u1tyrVt_I zJ$sVqjUqB{xPSD%Nm%KpoTMbBX+(^)rww&5xBaT$O$RQ~t{XK{$7;ai0A(&jY+AL} zOppL#!c=@9l9*}uvO~`IQO5N6Q0;meGm2i?0b#m+T1E} z`;k}3~Fb1q8x)=@FNKMNl* z_2kfA@Yn~Jw@_>bBc3er$P$mpX@1 z=h~Y?L?CWzz0iMEhU96b*^G4sWfL zluC@9n#(>uV38afd3gCt+VGGowLG%M1RVR{_UYh#5HFG0W8JeTHZdVNP7sWLWly&M zMN?(8Wk0y<*EWc$emQ67%-?QDkMCslRaRrV&E;WdDD~rL;dBX@C*yJz zRZe)g)6_(Vd2ObfD9ddB08LX&f4QDetRS<2%t?Q&%H7GEWK&b)h?r1^nr^MPV;lI+ zY5?jdgWQtbYD|%_q!*rbJ8y@ycYB|sN{ieNN{KS4#!e28xazGIme2P%hzf6M2p-}< zR0#4Fvf!ZErt3E`NVIr-EJS=Pl#iXV6HkH&GSssx(==O|aX|0ES#za(S$mIvkEiN{ zA$J(kARn5S(|~%>7kk@Gv76&LNtK~K;8t_9)%iZBHX=7E9n332WWZyAQ%@ zf2Js>Kv$>RHN{jTz)hVVLYGr$q1m$lTc_QSkeQmYn|uKypraFABVj_{b*q7hVa1Ap zO}`S{VI36Y7$lgedy^Q&)4n)s?{jVX+_SD>TZ>o`k&CIy1k2KpAgk7Xac*16Imrr{ zvvb{Qx7m;O^#~AGMG3SZP_G7vm}f5w zJU~q71nf_J2JQ^tGvqd^{v1L<$w|w;XZwLg-3cbb3h6OE_CS*qh7Te@irRFmd7~ZB z1DhZtBfG3>#|oG^Wj-Tgd5J`2aM4-%lcc+zRiUddzX)bLyAt6sBnZYDnb5NtRkDp2 zhv^?GLWjFU4MGlWGX?pMsF1M#i&u4Nrglv{-a3(s7(7z`#ee$OZdM)I-r9%K->sBZ6I&odly#Spj2n|^s z6_XZ^JtS02HH{71I2oPiAGgf6j(3t$%^{0oCmF&u2&&jq$%Zt!3kY#YA`wY_fEQE6 zQo&?k?8PlY=G2%(1)pRTk|T(Mb-IrsxeeX%xni1FxUj2Rjb~wwazB@Jg~~?a_OKK3 zeJ;3C3bA1q=T0?Y4-w-|%`QP6nc>Y`!}v#P#U> zYF#vh?4Jv0zH((GYDP17WWP#YXw``5$M+Ejs{m!3VMHg;3_+(S$b*;Sem%(z-4=6+ zB>-Pz`5)^Q&fmBjuw-Hf%%CbeH=6T_ZYZACi1$-f-B=2pah6Mz-M$Wci!a(2yPzI^j zcL9=T`SQYPJqquk0y->l_8teF{LSg}hub}OAmsk8+tb6kvJVD}=V8#1aj}Q1rzn2d z)Ahqr7h|5{zazw9C$lkYkt+5hU1Q(;d%jXNC9wFQSH}$qnq#q=xfICl!;Zpi6OBVWMyN^M{ME zov&1oVHEsqA(t*L#R^NVuuj}3!y8n9@R28yZgH~<^Rr?ulV`=?@KD%JuSX*jdJGSS z&Y6;A%)aaR35Uj~qv4}!bI1f}cyaVs@HM}Z+}Ejz-nbClRqglL*3}C^AO$#58y67p z33tmBbvQ%bg`0Xq#NW(WX`N=B!ysV2pB}fqPz|q*znu@DFVde#fv zg@31h0sKs4SvAVG@2ii4Iz)jiHR_9=Qu;-;hki;3Zh{9+WB1qf3vQX~^7nAyp5#g1 zsD}E7AD~U6^R^gHFuOUL%f~;TDdJ;u<#6X3+o_)4AQbL|M3nDWA@A-PWe;OXQTsO7 zV~MHu@B=Ih3hR}aq(L???|~9r1w%EgzdE7}d7F3pcVYRRhV$NcIIGl>$vp79bgy)l z+%4xnH~p6~cy1wJY{t9C1n2Vn!d@+nE8K+Gj*fn^@r>o|ny0;t7XGR0QmKRrC zCom^m%D+NYGz_;%i~_?@RoolnZVsKY826Aok2a4NL$vTBFq0AQX3j*NiWylz2HtiTrvI6d#3h7HrNe8Q}x%H20Dr(-KCcRMXQca z@35H|i@W=Wo;w}*s+V;PGneW&e8s5FXkO5Bz z-u;jc^*kMvNsG-ZYkHvD{7uwZ?DZw4;Bhel_QlEgfba&hPpAc0%AMcwihmvukMvLu zk7FfSu7wV8vfVge5@mJ`cWeFyliS|*`VawA8h3?ElHnC5H_#AzSPZNoa>luA3X#Tfn<}{WFruRXqtR6*d3+W58B8(c~jtL7#3B$pnb{)GNxIM~E z44$uE*>*E@@v{wov{yQEG6?3I*+C2AQa)sO0Rgcxc*@=dS0qsxfRFq_d zQ8%pI#yeeX1CplA;O(YqB*aBZlr(%WD<~vaLNctPo-%)GxU~W!GWRn1eE3#Hks)hd zYMQ15TWsOT5r@QemvgkbTmn&9j+1o`|-@ zvZe4}ghdsNntn^Pxi$-q+|C4jnN0#hZGlBUQ0C4y>fJ_)9Z2BEpFR^Fchw$b`fVsk=6~(h)I*T4N>)}TH}C{i$cozh1x=*gA)N7;D-6@e9LWZUvW4cL zJeN_0V6xRwYUekPP`)->zJVz57d;*c?i5^6(|1Xf&Cfy6NwSD|0(WWU#SUDNl#?0( zUluhMB3z1)bZD#%J?U6h&QIH0WHM(l9ZL&x% zFI95d0EF-zaM|M1NO5Z9dVnw=b#;n@R}GX;^+oEswea?Sa4WFzQ`=o#xnCTNhIMkShg!V$glnYx%dCP5@^*W&q+!Ea|F@ zLyw2Vpzg}mrG86OW{q()juKj>QhCb=!G63oI>beSG{uxq&gVc-<>lC#&b`Ve ziO;As@IL0xO0EvYf#~cS=S)3<{y`g@Hv&HvK>Pvry8uZN9*rusW_@qx<31OU6y@p(m7;L;+CJINI(J)n1x&2A&jrtZ1ZR=fV=$a3snVb9=vmPS92} zAy}4})mw({Q}FS}u6om)Y;Jipj+Qo-*nB#h34%1huLeLXB&O3JSVLGL z1}@?nNOWE9(TtCN%FE1rSr{LHfC+2Og-0{%Tb;Q*qGR zTU<>Pntg}oyR`p9<7Zu;V(pD>?b^n<-&06lQU|Kf*LSL_yG->_7~Sy1wAW1BN=tRF z7~P?xnf1~g)#G&Ps&qJ~661l0#c#6rFtauxS(BYH(6!urVhrrTY)15xYWWsAr4svz ztv%48-owgBS3)?Q2*cDREaxgJ1y?c1{D^91j^mm5{fSu>5dn?GPZcr5jpeeC?^~`5 zv#-^jE4@|g1I{7lm8k2g=*^)-e$ON)Ji8Je5`m=wi{vz}uob&0GS10O+SF)Zzx}*1 z8(WZ7>g`#X58DH?VkBI%qesA$ffh6RvO&at^O5+}$RV;TWi$1ou$}o=`@_B((J+Ls zwp#Rp%ovybZzeXWch6n$hZZ>*$O4j$>;Sgr@5pQOxp>vDcu|u^g~O{2Ew$?K9`)88 zjh@d6*PXMZ|8+_k(O2U3xiapK4cH#H)vUW={_Lm|JNiSC&(>z;o9+lOdF?jLaSdcP zrdTrj+>%kTU+SW6d<_sAZulL?Dui4t)=gD>DA95CiE*^{jB`%wul1gL^}O;3|76T9 z0)zmGX}kb|u4ty=;f`U7zy!KWm%b1RmDB9nV?1D6zP>+Me_#!p1COl1Qwq2a z3gWOP`Y_d|8)mC$OC!Bl`2TPvNReO_wGY*!a2O&Y>A!Z1P5_Mmu>Yg3$#)M2_rLVW z$^QT9k)9P#RS6_N_?V&g0G~+xU#GvaI%iY{V(dD1rO1|6STuEcnBR|E%VW1y~mZf+JpyH zpJ@AG=4^-`VyO9BDni`E=U7D~T!?Q65XOR3_;;0J)tHH2x@e!UG8794t8;ugQ{XZb z+8cfm{hq^8>Z!icFUsDZLE^r;+^Slc!N*lX2OocmR1F^PyDn36)(Gvt+`sHs7&*MX zz|mYhJ2oiT6wG?K_g^fxf4sr(NlSgDh7ve3oo;k5F#l{tp9o)<36bgNOkI?Du|u~v z<-=m>Z&F^x7#Tf6TfVXN=|MKO>5b(86`5skdBIFiA$oe*OI#l5wy5Tn+cC^K2Fx*Im$rdr3&m zo*B+3M^~gH6($(lMXO zBdw=vMvA7TUY9^R56#$2dYv%fAoB@km?P*(aV=>EA z=E0M1ALW!>=L1hyUu3*eR6p_&Gi$pcwppmtj25i39VIjxod{@AR&ox1THIRvwmKA< zRL)Q@ZB8j&Wj-AL)U^7?9%iMA^#*}r+m#@h_d$~D`w=SAKGpr2^%p4`#W^*V!h3!h zztz4hI|ZeGKp>30+?US0Ht&A-Qhw}4pz+rg3NFnrRa^Qz#ekWN1DjEdG|`1L9g^Q8 z*t(?gufuX&Hf>gtt0~8w99|Z*vfZhTb%#BDR=8)Ur^T3$({{#^A>gU%ETv{9y8^4E{F_xk&&rcXqDvk#Po{HNxgnngveGdljny+6r1R6nawek$h` z50z#y%mu@o>;z4;@mF(Jl2S^ElWH|~+qtN4TB-{LR%ywnsIjqWUU!9SIMj$AI1V;w z9ph}8r8J`XNYjcY8ZY`B+*Q_Cgbv%IUz<2#5A04r!lM4pYO|X%i^x!Yw_4;F zvhvLg!KY>#JhuXk2M0Ij_g6mOr)*}@8((B_z0LAF&xr{*H#yYrg4>pPLfikT+U~zq zbEol8ul*mue@a;@YYVbWmN14QS}a)-Gg&euyRs#w?7K*c$Zi-;WSwS=HG{EJD%+gw zOpK)*j&%+~G{XOK{!i|Q_tV?U%ah;CZ!XvNz2-IZd4C>L-=)5qTFA|{E({9IY8qm( zES8R4@iGyR(&v*mz=@>ti^9}A^K^a%E8!;AiuNL4QCu&>*U3Ly>V7Q0_cq>MRln_F zL${(&6OFsp)vdmJ)lN};?P`QohZdR_?WgiOOEI81)+$fQX89kUo2%EGCeSJHkm?qC z#l5THkqXU!8GjZ!TfOrKah5^}*r|t=`OOgY6vm6$`ZZlmMV$H-h2XC3nc&?S z*K7I$5)xgO6oi`OUoTun9jtIz_5Py$QekoA_eVK2aAX@|$DuCY+?+0|^rl~~5!EO@ zXVE**GF+nka8WsmlheF*VM_#4sv!CFQ%vHLQ8V61-rPs4wyRZ!+ETR(ZOBfutPb1=frz%2~>M3 z!Uo5mIwPe|Z1b7;IWv^gF=P+<8#1JbF7nc8>~{i??PPv>m7Q}m58ex1$B6wN+?bcw zp)a|aHPsiBb=rNV;4{3*FFUc-qKpz9eeC)LW#<^eCsH*RlRl}=X&qp9J#l8c*z#3wo{Kf`hBX)s1gO6IcDIS5#JIIMptT(fQg@bGh8t( zFYUh_Sn)F`0o^o^6s(fVC4c&*NO=n;CO^ZT*uTQhrM?vqveQdlA~hsd02 zRjAlKM3b%*F>2cJRPh>;c{AQ@AeD|aS0a&}YUkR`x0p++{;OI^dtA_&6h);R>0V2v zyN)kDQkI=#e(*_{uom4{Tbic2{>~bwiSgj`EX@kluar-t(&41+CnGIE262hz79}+; z3f{k?!g*{AlfLG?6_oig5siP(r z`ej(HJD6W*4sFb|BkrbIvC08*Dd2Ce`2$rGbr%a1iJj~Ex; z^)B1&vzy2cLdv^7HNxt7*qtrcMqz056SvBtL|D{&4sIbl zVWQE`c~|xG?ak%M5D0SS)8>3OSRIx{`TcF@_`+f6P{qFON0Is{@b;jSg;f>+VY%$U z5QDDs;=$i1;z$+<+SW}z1VO8@up_!Rx%4O_!q-YT3DSCcGr3!_ol<27 z9)z4W5rUxi(vf#~1Pdi{FUMj+=gKLTp`cts&X6`78J_|^Wb7wsF82he%tw6@`^ShC zY*E}$n2ILR6_wVjD332*e^nwbm7vQ7qKb449EAtvH<78_Dr*GdUj zW|>8NxxcQWv9aNK($f<=*N{5bWpGo@WAUtT7h2bePCi)IIm=ClqJFG9mkxEYypVrxl?sOhmj?AanIP!>*`|D1%bs^p8sySbF7SQqBk#|h~ zoqL3zk%`|}py})GSvDID?n>dvTJC&d}=y5^*xN99BG+}PabC<{Z-0tB{4ebT7W^mcu z!XhIe)Xl*@T8^bptM<;!DjNqt9jNzGAIfiV!)jScPdDz~j~oK&-~o>da$UV% zQcy!-H4K6M+lvfDkn*Jm)u)+k$u^Ngnjvx*wHHBl(yjKk06P2oEy%BN&Wf)5xcoY~?rafL^i|q+VFl_aBc04g^|R zh>@mCWjoQg{i*xBCBElQZY`<+Y02R#+w3u#n2B zWXl`j_lixmVWAuCv)to44)T>~qVb#VAS2KxS6D0m?UOXgGTVA3#%hz3O~VcW#krXh z7syh-jI%1dW7_xYp=oMP$f7Ku&0O-~po5M@j%kKSfLId9=2}-e-W&_dNI;C-vq+$N z-Gq9ST^BEYY4ySDP=-moEg-WS0&Jctib>iY3#(1?j=m==92uNL1niwAV<^*cNZ9ZuG*lzZ2!-pNSP`2{hp)tOuStJI*J zIXe){8UxM@1+$QxkwFCXo;{$SP&>Jzm_YNYl@~eojW=E<&P}!{!z_-o+gSTd2z<%C zW*YS?QPQ7j9KQO2%^EOLKzyk}J4%Cg3HVe#-RA9tNui#zagU7EU;t`h@%C}FH_fbC zzMNJo+}}O5g8tZLdAgXDdA=j{wCy$1^$Ov9Br2!PnE8Dh2q6;yG%F^NIEEgJ%!_IL zoC(>VC%r!^ixL8XY7oUEf(R^emk0l;-vxsx=ic|TU0G2rw|jZErghIbTJDI?y)`X; zetn#K`}T*1->KR{wrNDKP8@o!5QzeD=spm~t_j~|YT0i^?Xfr;-&+#X;GKuL#PCTOQCQsq=Et+5XZ-#Rj(z3>+ioG zXl#Fb-Z1@2uG;bQD`Q$Li`QDy=7>$hTKn0}nu;(Z-~@D%&Lj6BvRuYaN0hF3%BPqi z04z9EvCKg})>m`aOHaVVDU!M?sc;2SNTY2A)G7#aeBkloy4 zzzO!E_?EAl0|%^^1ZsA6cC`F0UA@=Lz#wiibVXx$Xh`NH=RtJ*fan6D*0R)8KCpNb z+SxZu9zQPoxiVE@*BG#nZeib#sc0rVR4#s~{CeGW7!UT3xI)d1BtP*Kkui?BsuUal<=VOBy=r$kL7&@AP~-vAXP43b_c;gWj;`BJgl2T{HIHsr>7m| zE#JZDo1j+Bya33JQSNmOUc98+-tS+drEDPhoSztE>q@-zhc=%-uIL>r(n46o! zWn`|3;K6t(drmez8exSXo}d(@F3mo)?C5a;FIA+V(?+4#ZVVcno+8$_fxS?kBB9?c z;H!#64FGY&AJCkxB?{o8R@PPTf`6LUjni?sTLYuo-DE`AR^yb*9Z(x#34k-ifV8nl zpw_#du=hYZsCn}1NBkLJx$-z)a_t$K{v2m+Y5BGCOOTt#^U3k?QUDUU?J_q7<$1u< zV?c7e+I<*q7q&UUrKs&oV}LEr;G*T@IzTK1^|yh3D} zWP+%MH3-2LfS_U>zbXXH*M1fU8A3l4o~J454H9qDfQRPW2CDabhG@_?mI**4 zzyaOV*h+;-N%c%O0)wGCfMIQvdGXmra7<)I6%>eT0PTYL_mx%>pg;V6)m6uR=>=nI zZevs8LdCg(VEkgc85j*8Q^3E8hAu0D@zDaNz+BjJus+`O5q$*cDjUE}z<`8wydet| zaor7|cwjth0ORBLfk(>US9j46Xfnm+iV}#)DefinitionData = array(); + + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + # remove surrounding line breaks + $text = trim($text, "\n"); + + # split text into lines + $lines = explode("\n", $text); + + # iterate through lines to identify blocks + $markup = $this->lines($lines); + + # trim line breaks + $markup = trim($markup, "\n"); + + return $markup; + } + + # + # Setters + # + + function setBreaksEnabled($breaksEnabled) + { + $this->breaksEnabled = $breaksEnabled; + + return $this; + } + + protected $breaksEnabled; + + function setMarkupEscaped($markupEscaped) + { + $this->markupEscaped = $markupEscaped; + + return $this; + } + + protected $markupEscaped; + + function setUrlsLinked($urlsLinked) + { + $this->urlsLinked = $urlsLinked; + + return $this; + } + + protected $urlsLinked = true; + + # + # Lines + # + + protected $BlockTypes = array( + '#' => array('Header'), + '*' => array('Rule', 'List'), + '+' => array('List'), + '-' => array('SetextHeader', 'Table', 'Rule', 'List'), + '0' => array('List'), + '1' => array('List'), + '2' => array('List'), + '3' => array('List'), + '4' => array('List'), + '5' => array('List'), + '6' => array('List'), + '7' => array('List'), + '8' => array('List'), + '9' => array('List'), + ':' => array('Table'), + '<' => array('Comment', 'Markup'), + '=' => array('SetextHeader'), + '>' => array('Quote'), + '[' => array('Reference'), + '_' => array('Rule'), + '`' => array('FencedCode'), + '|' => array('Table'), + '~' => array('FencedCode'), + ); + + # ~ + + protected $unmarkedBlockTypes = array( + 'Code', + ); + + # + # Blocks + # + + private function lines(array $lines) + { + $CurrentBlock = null; + + foreach ($lines as $line) + { + if (chop($line) === '') + { + if (isset($CurrentBlock)) + { + $CurrentBlock['interrupted'] = true; + } + + continue; + } + + if (strpos($line, "\t") !== false) + { + $parts = explode("\t", $line); + + $line = $parts[0]; + + unset($parts[0]); + + foreach ($parts as $part) + { + $shortage = 4 - mb_strlen($line, 'utf-8') % 4; + + $line .= str_repeat(' ', $shortage); + $line .= $part; + } + } + + $indent = 0; + + while (isset($line[$indent]) and $line[$indent] === ' ') + { + $indent ++; + } + + $text = $indent > 0 ? substr($line, $indent) : $line; + + # ~ + + $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); + + # ~ + + if (isset($CurrentBlock['continuable'])) + { + $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock); + + if (isset($Block)) + { + $CurrentBlock = $Block; + + continue; + } + else + { + if (method_exists($this, 'block'.$CurrentBlock['type'].'Complete')) + { + $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); + } + } + } + + # ~ + + $marker = $text[0]; + + # ~ + + $blockTypes = $this->unmarkedBlockTypes; + + if (isset($this->BlockTypes[$marker])) + { + foreach ($this->BlockTypes[$marker] as $blockType) + { + $blockTypes []= $blockType; + } + } + + # + # ~ + + foreach ($blockTypes as $blockType) + { + $Block = $this->{'block'.$blockType}($Line, $CurrentBlock); + + if (isset($Block)) + { + $Block['type'] = $blockType; + + if ( ! isset($Block['identified'])) + { + $Blocks []= $CurrentBlock; + + $Block['identified'] = true; + } + + if (method_exists($this, 'block'.$blockType.'Continue')) + { + $Block['continuable'] = true; + } + + $CurrentBlock = $Block; + + continue 2; + } + } + + # ~ + + if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted'])) + { + $CurrentBlock['element']['text'] .= "\n".$text; + } + else + { + $Blocks []= $CurrentBlock; + + $CurrentBlock = $this->paragraph($Line); + + $CurrentBlock['identified'] = true; + } + } + + # ~ + + if (isset($CurrentBlock['continuable']) and method_exists($this, 'block'.$CurrentBlock['type'].'Complete')) + { + $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); + } + + # ~ + + $Blocks []= $CurrentBlock; + + unset($Blocks[0]); + + # ~ + + $markup = ''; + + foreach ($Blocks as $Block) + { + if (isset($Block['hidden'])) + { + continue; + } + + $markup .= "\n"; + $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']); + } + + $markup .= "\n"; + + # ~ + + return $markup; + } + + # + # Code + + protected function blockCode($Line, $Block = null) + { + if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] >= 4) + { + $text = substr($Line['body'], 4); + + $Block = array( + 'element' => array( + 'name' => 'pre', + 'handler' => 'element', + 'text' => array( + 'name' => 'code', + 'text' => $text, + ), + ), + ); + + return $Block; + } + } + + protected function blockCodeContinue($Line, $Block) + { + if ($Line['indent'] >= 4) + { + if (isset($Block['interrupted'])) + { + $Block['element']['text']['text'] .= "\n"; + + unset($Block['interrupted']); + } + + $Block['element']['text']['text'] .= "\n"; + + $text = substr($Line['body'], 4); + + $Block['element']['text']['text'] .= $text; + + return $Block; + } + } + + protected function blockCodeComplete($Block) + { + $text = $Block['element']['text']['text']; + + $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); + + $Block['element']['text']['text'] = $text; + + return $Block; + } + + # + # Comment + + protected function blockComment($Line) + { + if ($this->markupEscaped) + { + return; + } + + if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!') + { + $Block = array( + 'markup' => $Line['body'], + ); + + if (preg_match('/-->$/', $Line['text'])) + { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + $Block['markup'] .= "\n" . $Line['body']; + + if (preg_match('/-->$/', $Line['text'])) + { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches)) + { + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if (isset($matches[1])) + { + $class = 'language-'.$matches[1]; + + $Element['attributes'] = array( + 'class' => $class, + ); + } + + $Block = array( + 'char' => $Line['text'][0], + 'element' => array( + 'name' => 'pre', + 'handler' => 'element', + 'text' => $Element, + ), + ); + + return $Block; + } + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['element']['text']['text'] .= "\n"; + + unset($Block['interrupted']); + } + + if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text'])) + { + $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['text']['text'] .= "\n".$Line['body'];; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + $text = $Block['element']['text']['text']; + + $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); + + $Block['element']['text']['text'] = $text; + + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + if (isset($Line['text'][1])) + { + $level = 1; + + while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') + { + $level ++; + } + + if ($level > 6) + { + return; + } + + $text = trim($Line['text'], '# '); + + $Block = array( + 'element' => array( + 'name' => 'h' . min(6, $level), + 'text' => $text, + 'handler' => 'line', + ), + ); + + return $Block; + } + } + + # + # List + + protected function blockList($Line) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]'); + + if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) + { + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'element' => array( + 'name' => $name, + 'handler' => 'elements', + ), + ); + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $matches[2], + ), + ); + + $Block['element']['text'] []= & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) + { + if (isset($Block['interrupted'])) + { + $Block['li']['text'] []= ''; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $text, + ), + ); + + $Block['element']['text'] []= & $Block['li']; + + return $Block; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) + { + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + + $Block['li']['text'] []= $text; + + return $Block; + } + + if ($Line['indent'] > 0) + { + $Block['li']['text'] []= ''; + + $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + + $Block['li']['text'] []= $text; + + unset($Block['interrupted']); + + return $Block; + } + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => 'lines', + 'text' => (array) $matches[1], + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + { + if (isset($Block['interrupted'])) + { + $Block['element']['text'] []= ''; + + unset($Block['interrupted']); + } + + $Block['element']['text'] []= $matches[1]; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $Block['element']['text'] []= $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text'])) + { + $Block = array( + 'element' => array( + 'name' => 'hr' + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + { + return; + } + + if (chop($Line['text'], $Line['text'][0]) === '') + { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped) + { + return; + } + + if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) + { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) + { + return; + } + + $Block = array( + 'name' => $matches[1], + 'depth' => 0, + 'markup' => $Line['text'], + ); + + $length = strlen($matches[0]); + + $remainder = substr($Line['text'], $length); + + if (trim($remainder) === '') + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + $Block['closed'] = true; + + $Block['void'] = true; + } + } + else + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + return; + } + + if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) + { + $Block['closed'] = true; + } + } + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open + { + $Block['depth'] ++; + } + + if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close + { + if ($Block['depth'] > 0) + { + $Block['depth'] --; + } + else + { + $Block['closed'] = true; + } + } + + if (isset($Block['interrupted'])) + { + $Block['markup'] .= "\n"; + + unset($Block['interrupted']); + } + + $Block['markup'] .= "\n".$Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (preg_match('/^\[(.+?)\]:[ ]*?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) + { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => null, + ); + + if (isset($matches[3])) + { + $Data['title'] = $matches[3]; + } + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'hidden' => true, + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, array $Block = null) + { + if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + { + return; + } + + if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') + { + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) + { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') + { + continue; + } + + $alignment = null; + + if ($dividerCell[0] === ':') + { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') + { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['text']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + foreach ($headerCells as $index => $headerCell) + { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'text' => $headerCell, + 'handler' => 'line', + ); + + if (isset($alignments[$index])) + { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => 'text-align: '.$alignment.';', + ); + } + + $HeaderElements []= $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'handler' => 'elements', + ), + ); + + $Block['element']['text'] []= array( + 'name' => 'thead', + 'handler' => 'elements', + ); + + $Block['element']['text'] []= array( + 'name' => 'tbody', + 'handler' => 'elements', + 'text' => array(), + ); + + $Block['element']['text'][0]['text'] []= array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $HeaderElements, + ); + + return $Block; + } + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) + { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches); + + foreach ($matches[0] as $index => $cell) + { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => 'line', + 'text' => $cell, + ); + + if (isset($Block['alignments'][$index])) + { + $Element['attributes'] = array( + 'style' => 'text-align: '.$Block['alignments'][$index].';', + ); + } + + $Elements []= $Element; + } + + $Element = array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $Elements, + ); + + $Block['element']['text'][1]['text'] []= $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + $Block = array( + 'element' => array( + 'name' => 'p', + 'text' => $Line['text'], + 'handler' => 'line', + ), + ); + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '"' => array('SpecialCharacter'), + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), + '>' => array('SpecialCharacter'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!"*_&[:<>`~\\'; + + # + # ~ + # + + public function line($text) + { + $markup = ''; + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) + { + $marker = $excerpt[0]; + + $markerPosition = strpos($text, $marker); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) + { + $Inline = $this->{'inline'.$inlineType}($Excerpt); + + if ( ! isset($Inline)) + { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) + { + continue; + } + + # sets a default inline position + + if ( ! isset($Inline['position'])) + { + $Inline['position'] = $markerPosition; + } + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $markup .= $this->unmarkedText($unmarkedText); + + # compile the inline + $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $markup .= $this->unmarkedText($unmarkedText); + + $text = substr($text, $markerPosition + 1); + } + + $markup .= $this->unmarkedText($text); + + return $markup; + } + + # + # ~ + # + + protected function inlineCode($Excerpt) + { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(? strlen($matches[0]), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + if ( ! isset($matches[2])) + { + $url = 'mailto:' . $url; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'strong'; + } + elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'em'; + } + else + { + return; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => $emphasis, + 'handler' => 'line', + 'text' => $matches[1], + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) + { + return array( + 'markup' => $Excerpt['text'][1], + 'extent' => 2, + ); + } + } + + protected function inlineImage($Excerpt) + { + if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') + { + return; + } + + $Excerpt['text']= substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) + { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['text'], + ), + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => 'line', + 'text' => null, + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]|(?R))*)\]/', $remainder, $matches)) + { + $Element['text'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } + else + { + return; + } + + if (preg_match('/^[(]((?:[^ ()]|[(][^ )]+[)])+)(?:[ ]+("[^"]*"|\'[^\']*\'))?[)]/', $remainder, $matches)) + { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) + { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } + else + { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) + { + $definition = strlen($matches[1]) ? $matches[1] : $Element['text']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } + else + { + $definition = strtolower($Element['text']); + } + + if ( ! isset($this->DefinitionData['Reference'][$definition])) + { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + $Element['attributes']['href'] = str_replace(array('&', '<'), array('&', '<'), $Element['attributes']['href']); + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false) + { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text'])) + { + return array( + 'markup' => '&', + 'extent' => 1, + ); + } + + $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); + + if (isset($SpecialCharacter[$Excerpt['text'][0]])) + { + return array( + 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';', + 'extent' => 1, + ); + } + } + + protected function inlineStrikethrough($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) + { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'text' => $matches[1], + 'handler' => 'line', + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') + { + return; + } + + if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) + { + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $matches[0][0], + 'attributes' => array( + 'href' => $matches[0][0], + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) + { + $url = str_replace(array('&', '<'), array('&', '<'), $matches[1]); + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + if ($this->breaksEnabled) + { + $text = preg_replace('/[ ]*\n/', "
\n", $text); + } + else + { + $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "
\n", $text); + $text = str_replace(" \n", "\n", $text); + } + + return $text; + } + + # + # Handlers + # + + protected function element(array $Element) + { + $markup = '<'.$Element['name']; + + if (isset($Element['attributes'])) + { + foreach ($Element['attributes'] as $name => $value) + { + if ($value === null) + { + continue; + } + + $markup .= ' '.$name.'="'.$value.'"'; + } + } + + if (isset($Element['text'])) + { + $markup .= '>'; + + if (isset($Element['handler'])) + { + $markup .= $this->{$Element['handler']}($Element['text']); + } + else + { + $markup .= $Element['text']; + } + + $markup .= ''; + } + else + { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + foreach ($Elements as $Element) + { + $markup .= "\n" . $this->element($Element); + } + + $markup .= "\n"; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $markup = $this->lines($lines); + + $trimmedMarkup = trim($markup); + + if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '

') + { + $markup = $trimmedMarkup; + $markup = substr($markup, 3); + + $position = strpos($markup, "

"); + + $markup = substr_replace($markup, '', $position, 4); + } + + return $markup; + } + + # + # Deprecated Methods + # + + function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + # + # Static Methods + # + + static function instance($name = 'default') + { + if (isset(self::$instances[$name])) + { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'sub', 'mark', + 'u', 'xm', 'sup', 'nobr', + 'var', 'ruby', + 'wbr', 'span', + 'time', + ); +} diff --git a/vendor/erusev/parsedown/README.md b/vendor/erusev/parsedown/README.md new file mode 100644 index 0000000..6f9f649 --- /dev/null +++ b/vendor/erusev/parsedown/README.md @@ -0,0 +1,57 @@ +## Parsedown + +[![Build Status](https://img.shields.io/travis/erusev/parsedown/master.svg?style=flat-square)](https://travis-ci.org/erusev/parsedown) + + +Better Markdown Parser in PHP + +[Demo](http://parsedown.org/demo) | +[Benchmarks](http://parsedown.org/speed) | +[Tests](http://parsedown.org/tests/) | +[Documentation](https://github.com/erusev/parsedown/wiki/) + +### Features + +* Super Fast +* [GitHub flavored](https://help.github.com/articles/github-flavored-markdown) +* Extensible +* Tested in 5.3 to 5.6 +* [Markdown Extra extension](https://github.com/erusev/parsedown-extra) + +### Installation + +Include `Parsedown.php` or install [the composer package](https://packagist.org/packages/erusev/parsedown). + +### Example + +``` php +$Parsedown = new Parsedown(); + +echo $Parsedown->text('Hello _Parsedown_!'); # prints:

Hello Parsedown!

+``` + +More examples in [the wiki](https://github.com/erusev/parsedown/wiki/) and in [this video tutorial](http://youtu.be/wYZBY8DEikI). + +### Questions + +**How does Parsedown work?** + +It tries to read Markdown like a human. First, it looks at the lines. It’s interested in how the lines start. This helps it recognise blocks. It knows, for example, that if a line start with a `-` then it perhaps belong to a list. Once it recognises the blocks, it continues to the content. As it reads, it watches out for special characters. This helps it recognise inline elements (or inlines). + +We call this approach "line based". We believe that Parsedown is the first Markdown parser to use it. Since the release of Parsedown, other developers have used the same approach to develop other Markdown parsers in PHP and in other languages. + +**Is it compliant with CommonMark?** + +It passes most of the CommonMark tests. Most of the tests that don't pass deal with cases that are quite uncommon. Still, as CommonMark matures, compliance should improve. + +**Who uses it?** + +[phpDocumentor](http://www.phpdoc.org/), [October CMS](http://octobercms.com/), [Bolt CMS](http://bolt.cm/), [Kirby CMS](http://getkirby.com/), [Grav CMS](http://getgrav.org/), [Statamic CMS](http://www.statamic.com/), [RaspberryPi.org](http://www.raspberrypi.org/) and [more](https://www.versioneye.com/php/erusev:parsedown/references). + +**How can I help?** + +Use it, star it, share it and if you feel generous, [donate](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=528P3NZQMP8N2). + +--- + +You might also like [Caret](http://caret.io) - our Markdown editor for the desktop. diff --git a/vendor/erusev/parsedown/composer.json b/vendor/erusev/parsedown/composer.json new file mode 100644 index 0000000..1439b82 --- /dev/null +++ b/vendor/erusev/parsedown/composer.json @@ -0,0 +1,18 @@ +{ + "name": "erusev/parsedown", + "description": "Parser for Markdown.", + "keywords": ["markdown", "parser"], + "homepage": "http://parsedown.org", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "autoload": { + "psr-0": {"Parsedown": ""} + } +} \ No newline at end of file diff --git a/vendor/erusev/parsedown/phpunit.xml.dist b/vendor/erusev/parsedown/phpunit.xml.dist new file mode 100644 index 0000000..b2d5e9d --- /dev/null +++ b/vendor/erusev/parsedown/phpunit.xml.dist @@ -0,0 +1,8 @@ + + + + + test/ParsedownTest.php + + + \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/CommonMarkTest.php b/vendor/erusev/parsedown/test/CommonMarkTest.php new file mode 100644 index 0000000..9b8d116 --- /dev/null +++ b/vendor/erusev/parsedown/test/CommonMarkTest.php @@ -0,0 +1,74 @@ +setUrlsLinked(false); + + $actualHtml = $Parsedown->text($markdown); + $actualHtml = $this->normalizeMarkup($actualHtml); + + $this->assertEquals($expectedHtml, $actualHtml); + } + + function data() + { + $spec = file_get_contents(self::SPEC_URL); + $spec = strstr($spec, '', true); + + $tests = array(); + $currentSection = ''; + + preg_replace_callback( + '/^\.\n([\s\S]*?)^\.\n([\s\S]*?)^\.$|^#{1,6} *(.*)$/m', + function($matches) use ( & $tests, & $currentSection, & $testCount) { + if (isset($matches[3]) and $matches[3]) { + $currentSection = $matches[3]; + } else { + $testCount++; + $markdown = $matches[1]; + $markdown = preg_replace('/→/', "\t", $markdown); + $expectedHtml = $matches[2]; + $expectedHtml = $this->normalizeMarkup($expectedHtml); + $tests []= array( + $currentSection, # section + $markdown, # markdown + $expectedHtml, # html + ); + } + }, + $spec + ); + + return $tests; + } + + private function normalizeMarkup($markup) + { + $markup = preg_replace("/\n+/", "\n", $markup); + $markup = preg_replace('/^\s+/m', '', $markup); + $markup = preg_replace('/^((?:<[\w]+>)+)\n/m', '$1', $markup); + $markup = preg_replace('/\n((?:<\/[\w]+>)+)$/m', '$1', $markup); + $markup = trim($markup); + + return $markup; + } +} diff --git a/vendor/erusev/parsedown/test/ParsedownTest.php b/vendor/erusev/parsedown/test/ParsedownTest.php new file mode 100644 index 0000000..c922ab1 --- /dev/null +++ b/vendor/erusev/parsedown/test/ParsedownTest.php @@ -0,0 +1,159 @@ +dirs = $this->initDirs(); + $this->Parsedown = $this->initParsedown(); + + parent::__construct($name, $data, $dataName); + } + + private $dirs, $Parsedown; + + /** + * @return array + */ + protected function initDirs() + { + $dirs []= dirname(__FILE__).'/data/'; + + return $dirs; + } + + /** + * @return Parsedown + */ + protected function initParsedown() + { + $Parsedown = new Parsedown(); + + return $Parsedown; + } + + /** + * @dataProvider data + * @param $test + * @param $dir + */ + function test_($test, $dir) + { + $markdown = file_get_contents($dir . $test . '.md'); + + $expectedMarkup = file_get_contents($dir . $test . '.html'); + + $expectedMarkup = str_replace("\r\n", "\n", $expectedMarkup); + $expectedMarkup = str_replace("\r", "\n", $expectedMarkup); + + $actualMarkup = $this->Parsedown->text($markdown); + + $this->assertEquals($expectedMarkup, $actualMarkup); + } + + function data() + { + $data = array(); + + foreach ($this->dirs as $dir) + { + $Folder = new DirectoryIterator($dir); + + foreach ($Folder as $File) + { + /** @var $File DirectoryIterator */ + + if ( ! $File->isFile()) + { + continue; + } + + $filename = $File->getFilename(); + + $extension = pathinfo($filename, PATHINFO_EXTENSION); + + if ($extension !== 'md') + { + continue; + } + + $basename = $File->getBasename('.md'); + + if (file_exists($dir . $basename . '.html')) + { + $data []= array($basename, $dir); + } + } + } + + return $data; + } + + public function test_no_markup() + { + $markdownWithHtml = <<_content_ + +sparse: + +
+
+_content_ +
+
+ +paragraph + + + +comment + + +MARKDOWN_WITH_MARKUP; + + $expectedHtml = <<<div>content</div>

+

sparse:

+

<div> +<div class="inner"> +content +</div> +</div>

+

paragraph

+

<style type="text/css"> +p { +color: red; +} +</style>

+

comment

+

<!-- html comment -->

+EXPECTED_HTML; + $parsedownWithNoMarkup = new Parsedown(); + $parsedownWithNoMarkup->setMarkupEscaped(true); + $this->assertEquals($expectedHtml, $parsedownWithNoMarkup->text($markdownWithHtml)); + } + + public function testLateStaticBinding() + { + include 'test/TestParsedown.php'; + + $parsedown = Parsedown::instance(); + $this->assertInstanceOf('Parsedown', $parsedown); + + // After instance is already called on Parsedown + // subsequent calls with the same arguments return the same instance + $sameParsedown = TestParsedown::instance(); + $this->assertInstanceOf('Parsedown', $sameParsedown); + $this->assertSame($parsedown, $sameParsedown); + + $testParsedown = TestParsedown::instance('test late static binding'); + $this->assertInstanceOf('TestParsedown', $testParsedown); + + $sameInstanceAgain = TestParsedown::instance('test late static binding'); + $this->assertSame($testParsedown, $sameInstanceAgain); + } +} diff --git a/vendor/erusev/parsedown/test/TestParsedown.php b/vendor/erusev/parsedown/test/TestParsedown.php new file mode 100644 index 0000000..7024dfb --- /dev/null +++ b/vendor/erusev/parsedown/test/TestParsedown.php @@ -0,0 +1,5 @@ + + + +header 1 +header 2 + + + + +cell 1.1 +cell 1.2 + + +cell 2.1 +cell 2.2 + + + \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/aesthetic_table.md b/vendor/erusev/parsedown/test/data/aesthetic_table.md new file mode 100644 index 0000000..5245e6c --- /dev/null +++ b/vendor/erusev/parsedown/test/data/aesthetic_table.md @@ -0,0 +1,4 @@ +| header 1 | header 2 | +| -------- | -------- | +| cell 1.1 | cell 1.2 | +| cell 2.1 | cell 2.2 | \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/aligned_table.html b/vendor/erusev/parsedown/test/data/aligned_table.html new file mode 100644 index 0000000..c4acfcb --- /dev/null +++ b/vendor/erusev/parsedown/test/data/aligned_table.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + +
header 1header 2header 2
cell 1.1cell 1.2cell 1.3
cell 2.1cell 2.2cell 2.3
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/aligned_table.md b/vendor/erusev/parsedown/test/data/aligned_table.md new file mode 100644 index 0000000..69a45f9 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/aligned_table.md @@ -0,0 +1,4 @@ +| header 1 | header 2 | header 2 | +| :------- | :------: | -------: | +| cell 1.1 | cell 1.2 | cell 1.3 | +| cell 2.1 | cell 2.2 | cell 2.3 | \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/atx_heading.html b/vendor/erusev/parsedown/test/data/atx_heading.html new file mode 100644 index 0000000..751f873 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/atx_heading.html @@ -0,0 +1,9 @@ +

h1

+

h2

+

h3

+

h4

+
h5
+
h6
+

####### not a heading

+

closed h1

+

#

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/atx_heading.md b/vendor/erusev/parsedown/test/data/atx_heading.md new file mode 100644 index 0000000..ad97b44 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/atx_heading.md @@ -0,0 +1,17 @@ +# h1 + +## h2 + +### h3 + +#### h4 + +##### h5 + +###### h6 + +####### not a heading + +# closed h1 # + +# \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/automatic_link.html b/vendor/erusev/parsedown/test/data/automatic_link.html new file mode 100644 index 0000000..50a94ba --- /dev/null +++ b/vendor/erusev/parsedown/test/data/automatic_link.html @@ -0,0 +1 @@ +

http://example.com

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/automatic_link.md b/vendor/erusev/parsedown/test/data/automatic_link.md new file mode 100644 index 0000000..08d3bf4 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/automatic_link.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/block-level_html.html b/vendor/erusev/parsedown/test/data/block-level_html.html new file mode 100644 index 0000000..6443a4a --- /dev/null +++ b/vendor/erusev/parsedown/test/data/block-level_html.html @@ -0,0 +1,12 @@ +
_content_
+

paragraph

+
+
+ _content_ +
+
+ +
+ home
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/block-level_html.md b/vendor/erusev/parsedown/test/data/block-level_html.md new file mode 100644 index 0000000..17cbc22 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/block-level_html.md @@ -0,0 +1,16 @@ +
_content_
+ +paragraph + +
+
+ _content_ +
+
+ + + +
+ home
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/code_block.html b/vendor/erusev/parsedown/test/data/code_block.html new file mode 100644 index 0000000..889b02d --- /dev/null +++ b/vendor/erusev/parsedown/test/data/code_block.html @@ -0,0 +1,8 @@ +
<?php
+
+$message = 'Hello World!';
+echo $message;
+
+
> not a quote
+- not a list item
+[not a reference]: http://foo.com
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/code_block.md b/vendor/erusev/parsedown/test/data/code_block.md new file mode 100644 index 0000000..2cfc953 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/code_block.md @@ -0,0 +1,10 @@ + not a quote + - not a list item + [not a reference]: http://foo.com \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/code_span.html b/vendor/erusev/parsedown/test/data/code_span.html new file mode 100644 index 0000000..5c4c231 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/code_span.html @@ -0,0 +1,6 @@ +

a code span

+

this is also a codespan trailing text

+

and look at this one!

+

single backtick in a code span: `

+

backtick-delimited string in a code span: `foo`

+

sth `` sth

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/code_span.md b/vendor/erusev/parsedown/test/data/code_span.md new file mode 100644 index 0000000..c2f1a74 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/code_span.md @@ -0,0 +1,11 @@ +a `code span` + +`this is also a codespan` trailing text + +`and look at this one!` + +single backtick in a code span: `` ` `` + +backtick-delimited string in a code span: `` `foo` `` + +`sth `` sth` \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/compound_blockquote.html b/vendor/erusev/parsedown/test/data/compound_blockquote.html new file mode 100644 index 0000000..37afb57 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/compound_blockquote.html @@ -0,0 +1,9 @@ +
+

header

+

paragraph

+
    +
  • li
  • +
+
+

paragraph

+
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/compound_blockquote.md b/vendor/erusev/parsedown/test/data/compound_blockquote.md new file mode 100644 index 0000000..80c4aed --- /dev/null +++ b/vendor/erusev/parsedown/test/data/compound_blockquote.md @@ -0,0 +1,10 @@ +> header +> ------ +> +> paragraph +> +> - li +> +> --- +> +> paragraph \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/compound_emphasis.html b/vendor/erusev/parsedown/test/data/compound_emphasis.html new file mode 100644 index 0000000..178dd54 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/compound_emphasis.html @@ -0,0 +1,2 @@ +

code code

+

codecodecode

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/compound_emphasis.md b/vendor/erusev/parsedown/test/data/compound_emphasis.md new file mode 100644 index 0000000..6fe07f2 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/compound_emphasis.md @@ -0,0 +1,4 @@ +_`code`_ __`code`__ + +*`code`**`code`**`code`* + diff --git a/vendor/erusev/parsedown/test/data/compound_list.html b/vendor/erusev/parsedown/test/data/compound_list.html new file mode 100644 index 0000000..f5593c1 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/compound_list.html @@ -0,0 +1,12 @@ +
    +
  • +

    paragraph

    +

    paragraph

    +
  • +
  • +

    paragraph

    +
    +

    quote

    +
    +
  • +
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/compound_list.md b/vendor/erusev/parsedown/test/data/compound_list.md new file mode 100644 index 0000000..ed7f0c6 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/compound_list.md @@ -0,0 +1,7 @@ +- paragraph + + paragraph + +- paragraph + + > quote \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/deeply_nested_list.html b/vendor/erusev/parsedown/test/data/deeply_nested_list.html new file mode 100644 index 0000000..d2c7e5a --- /dev/null +++ b/vendor/erusev/parsedown/test/data/deeply_nested_list.html @@ -0,0 +1,12 @@ +
    +
  • li +
      +
    • li +
        +
      • li
      • +
      • li
      • +
    • +
    • li
    • +
  • +
  • li
  • +
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/deeply_nested_list.md b/vendor/erusev/parsedown/test/data/deeply_nested_list.md new file mode 100644 index 0000000..76b7552 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/deeply_nested_list.md @@ -0,0 +1,6 @@ +- li + - li + - li + - li + - li +- li \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/em_strong.html b/vendor/erusev/parsedown/test/data/em_strong.html new file mode 100644 index 0000000..323d60a --- /dev/null +++ b/vendor/erusev/parsedown/test/data/em_strong.html @@ -0,0 +1,8 @@ +

em strong

+

em strong strong

+

strong em strong

+

strong em strong strong

+

em strong

+

em strong strong

+

strong em strong

+

strong em strong strong

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/em_strong.md b/vendor/erusev/parsedown/test/data/em_strong.md new file mode 100644 index 0000000..9abeb3f --- /dev/null +++ b/vendor/erusev/parsedown/test/data/em_strong.md @@ -0,0 +1,15 @@ +___em strong___ + +___em strong_ strong__ + +__strong _em strong___ + +__strong _em strong_ strong__ + +***em strong*** + +***em strong* strong** + +**strong *em strong*** + +**strong *em strong* strong** \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/email.html b/vendor/erusev/parsedown/test/data/email.html new file mode 100644 index 0000000..c40759c --- /dev/null +++ b/vendor/erusev/parsedown/test/data/email.html @@ -0,0 +1 @@ +

my email is me@example.com

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/email.md b/vendor/erusev/parsedown/test/data/email.md new file mode 100644 index 0000000..26b7b6c --- /dev/null +++ b/vendor/erusev/parsedown/test/data/email.md @@ -0,0 +1 @@ +my email is \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/emphasis.html b/vendor/erusev/parsedown/test/data/emphasis.html new file mode 100644 index 0000000..60ff4bd --- /dev/null +++ b/vendor/erusev/parsedown/test/data/emphasis.html @@ -0,0 +1,8 @@ +

underscore, asterisk, one two, three four, a, b

+

strong and em and strong and em

+

line +line +line

+

this_is_not_an_emphasis

+

an empty emphasis __ ** is not an emphasis

+

*mixed *double and single asterisk** spans

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/emphasis.md b/vendor/erusev/parsedown/test/data/emphasis.md new file mode 100644 index 0000000..85b9d22 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/emphasis.md @@ -0,0 +1,13 @@ +_underscore_, *asterisk*, _one two_, *three four*, _a_, *b* + +**strong** and *em* and **strong** and *em* + +_line +line +line_ + +this_is_not_an_emphasis + +an empty emphasis __ ** is not an emphasis + +*mixed **double and* single asterisk** spans \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/escaping.html b/vendor/erusev/parsedown/test/data/escaping.html new file mode 100644 index 0000000..ab1c41f --- /dev/null +++ b/vendor/erusev/parsedown/test/data/escaping.html @@ -0,0 +1,6 @@ +

escaped *emphasis*.

+

escaped \*emphasis\* in a code span

+
escaped \*emphasis\* in a code block
+

\ ` * _ { } [ ] ( ) > # + - . !

+

one_two one_two

+

one*two one*two

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/escaping.md b/vendor/erusev/parsedown/test/data/escaping.md new file mode 100644 index 0000000..9f174e9 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/escaping.md @@ -0,0 +1,11 @@ +escaped \*emphasis\*. + +`escaped \*emphasis\* in a code span` + + escaped \*emphasis\* in a code block + +\\ \` \* \_ \{ \} \[ \] \( \) \> \# \+ \- \. \! + +_one\_two_ __one\_two__ + +*one\*two* **one\*two** \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/fenced_code_block.html b/vendor/erusev/parsedown/test/data/fenced_code_block.html new file mode 100644 index 0000000..8bdabba --- /dev/null +++ b/vendor/erusev/parsedown/test/data/fenced_code_block.html @@ -0,0 +1,6 @@ +
<?php
+
+$message = 'fenced code block';
+echo $message;
+
tilde
+
echo 'language identifier';
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/fenced_code_block.md b/vendor/erusev/parsedown/test/data/fenced_code_block.md new file mode 100644 index 0000000..cbed8eb --- /dev/null +++ b/vendor/erusev/parsedown/test/data/fenced_code_block.md @@ -0,0 +1,14 @@ +``` + +
+
+
+
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/horizontal_rule.md b/vendor/erusev/parsedown/test/data/horizontal_rule.md new file mode 100644 index 0000000..bf461a9 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/horizontal_rule.md @@ -0,0 +1,9 @@ +--- + +- - - + + - - - + +*** + +___ \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/html_comment.html b/vendor/erusev/parsedown/test/data/html_comment.html new file mode 100644 index 0000000..566dc3a --- /dev/null +++ b/vendor/erusev/parsedown/test/data/html_comment.html @@ -0,0 +1,5 @@ + +

paragraph

+ +

paragraph

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/html_comment.md b/vendor/erusev/parsedown/test/data/html_comment.md new file mode 100644 index 0000000..6ddfdb4 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/html_comment.md @@ -0,0 +1,8 @@ + + +paragraph + + + +paragraph \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/html_entity.html b/vendor/erusev/parsedown/test/data/html_entity.html new file mode 100644 index 0000000..4d23e3c --- /dev/null +++ b/vendor/erusev/parsedown/test/data/html_entity.html @@ -0,0 +1 @@ +

& © {

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/html_entity.md b/vendor/erusev/parsedown/test/data/html_entity.md new file mode 100644 index 0000000..ff545ea --- /dev/null +++ b/vendor/erusev/parsedown/test/data/html_entity.md @@ -0,0 +1 @@ +& © { \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/image_reference.html b/vendor/erusev/parsedown/test/data/image_reference.html new file mode 100644 index 0000000..67fbd2c --- /dev/null +++ b/vendor/erusev/parsedown/test/data/image_reference.html @@ -0,0 +1,2 @@ +

Markdown Logo

+

![missing reference]

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/image_reference.md b/vendor/erusev/parsedown/test/data/image_reference.md new file mode 100644 index 0000000..1e11d94 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/image_reference.md @@ -0,0 +1,5 @@ +![Markdown Logo][image] + +[image]: /md.png + +![missing reference] \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/image_title.html b/vendor/erusev/parsedown/test/data/image_title.html new file mode 100644 index 0000000..957c950 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/image_title.html @@ -0,0 +1,2 @@ +

alt

+

blank title

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/image_title.md b/vendor/erusev/parsedown/test/data/image_title.md new file mode 100644 index 0000000..7ce2849 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/image_title.md @@ -0,0 +1,3 @@ +![alt](/md.png "title") + +![blank title](/md.png "") \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/implicit_reference.html b/vendor/erusev/parsedown/test/data/implicit_reference.html new file mode 100644 index 0000000..24b51c1 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/implicit_reference.html @@ -0,0 +1,4 @@ +

an implicit reference link

+

an implicit reference link with an empty link definition

+

an implicit reference link followed by another

+

an explicit reference link with a title

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/implicit_reference.md b/vendor/erusev/parsedown/test/data/implicit_reference.md new file mode 100644 index 0000000..f850df9 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/implicit_reference.md @@ -0,0 +1,13 @@ +an [implicit] reference link + +[implicit]: http://example.com + +an [implicit][] reference link with an empty link definition + +an [implicit][] reference link followed by [another][] + +[another]: http://cnn.com + +an [explicit][example] reference link with a title + +[example]: http://example.com "Example" \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/inline_link.html b/vendor/erusev/parsedown/test/data/inline_link.html new file mode 100644 index 0000000..5ad564a --- /dev/null +++ b/vendor/erusev/parsedown/test/data/inline_link.html @@ -0,0 +1,6 @@ +

link

+

link with parentheses in URL

+

(link) in parentheses

+

link

+

MD Logo

+

MD Logo and text

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/inline_link.md b/vendor/erusev/parsedown/test/data/inline_link.md new file mode 100644 index 0000000..6bac0b3 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/inline_link.md @@ -0,0 +1,11 @@ +[link](http://example.com) + +[link](/url-(parentheses)) with parentheses in URL + +([link](/index.php)) in parentheses + +[`link`](http://example.com) + +[![MD Logo](http://parsedown.org/md.png)](http://example.com) + +[![MD Logo](http://parsedown.org/md.png) and text](http://example.com) \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/inline_link_title.html b/vendor/erusev/parsedown/test/data/inline_link_title.html new file mode 100644 index 0000000..ecdfd03 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/inline_link_title.html @@ -0,0 +1,6 @@ +

single quotes

+

double quotes

+

single quotes blank

+

double quotes blank

+

space

+

parentheses

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/inline_link_title.md b/vendor/erusev/parsedown/test/data/inline_link_title.md new file mode 100644 index 0000000..6e1c5af --- /dev/null +++ b/vendor/erusev/parsedown/test/data/inline_link_title.md @@ -0,0 +1,11 @@ +[single quotes](http://example.com 'Title') + +[double quotes](http://example.com "Title") + +[single quotes blank](http://example.com '') + +[double quotes blank](http://example.com "") + +[space](http://example.com "2 Words") + +[parentheses](http://example.com/url-(parentheses) "Title") \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/inline_title.html b/vendor/erusev/parsedown/test/data/inline_title.html new file mode 100644 index 0000000..bbab93b --- /dev/null +++ b/vendor/erusev/parsedown/test/data/inline_title.html @@ -0,0 +1 @@ +

single quotes and double quotes

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/inline_title.md b/vendor/erusev/parsedown/test/data/inline_title.md new file mode 100644 index 0000000..cb09344 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/inline_title.md @@ -0,0 +1 @@ +[single quotes](http://example.com 'Example') and [double quotes](http://example.com "Example") \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/lazy_blockquote.html b/vendor/erusev/parsedown/test/data/lazy_blockquote.html new file mode 100644 index 0000000..0a2a2aa --- /dev/null +++ b/vendor/erusev/parsedown/test/data/lazy_blockquote.html @@ -0,0 +1,6 @@ +
+

quote +the rest of it

+

another paragraph +the rest of it

+
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/lazy_blockquote.md b/vendor/erusev/parsedown/test/data/lazy_blockquote.md new file mode 100644 index 0000000..48f645f --- /dev/null +++ b/vendor/erusev/parsedown/test/data/lazy_blockquote.md @@ -0,0 +1,5 @@ +> quote +the rest of it + +> another paragraph +the rest of it \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/lazy_list.html b/vendor/erusev/parsedown/test/data/lazy_list.html new file mode 100644 index 0000000..1a51992 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/lazy_list.html @@ -0,0 +1,4 @@ +
    +
  • li +the rest of it
  • +
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/lazy_list.md b/vendor/erusev/parsedown/test/data/lazy_list.md new file mode 100644 index 0000000..62ad9d7 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/lazy_list.md @@ -0,0 +1,2 @@ +- li +the rest of it \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/line_break.html b/vendor/erusev/parsedown/test/data/line_break.html new file mode 100644 index 0000000..5f37d85 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/line_break.html @@ -0,0 +1,2 @@ +

line
+line

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/line_break.md b/vendor/erusev/parsedown/test/data/line_break.md new file mode 100644 index 0000000..04dff43 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/line_break.md @@ -0,0 +1,2 @@ +line +line \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/multiline_list_paragraph.html b/vendor/erusev/parsedown/test/data/multiline_list_paragraph.html new file mode 100644 index 0000000..3247bd2 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/multiline_list_paragraph.html @@ -0,0 +1,7 @@ +
    +
  • +

    li

    +

    line +line

    +
  • +
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/multiline_list_paragraph.md b/vendor/erusev/parsedown/test/data/multiline_list_paragraph.md new file mode 100644 index 0000000..f5b4272 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/multiline_list_paragraph.md @@ -0,0 +1,4 @@ +- li + + line + line \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/nested_block-level_html.html b/vendor/erusev/parsedown/test/data/nested_block-level_html.html new file mode 100644 index 0000000..bfbef54 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/nested_block-level_html.html @@ -0,0 +1,10 @@ +
+_parent_ +
+_child_ +
+
+_adopted child_
+
+
+

outside

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/nested_block-level_html.md b/vendor/erusev/parsedown/test/data/nested_block-level_html.md new file mode 100644 index 0000000..5e01e10 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/nested_block-level_html.md @@ -0,0 +1,11 @@ +
+_parent_ +
+_child_ +
+
+_adopted child_
+
+
+ +_outside_ \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/ordered_list.html b/vendor/erusev/parsedown/test/data/ordered_list.html new file mode 100644 index 0000000..b6c5216 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/ordered_list.html @@ -0,0 +1,13 @@ +
    +
  1. one
  2. +
  3. two
  4. +
+

repeating numbers:

+
    +
  1. one
  2. +
  3. two
  4. +
+

large numbers:

+
    +
  1. one
  2. +
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/ordered_list.md b/vendor/erusev/parsedown/test/data/ordered_list.md new file mode 100644 index 0000000..b307032 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/ordered_list.md @@ -0,0 +1,11 @@ +1. one +2. two + +repeating numbers: + +1. one +1. two + +large numbers: + +123. one \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/paragraph_list.html b/vendor/erusev/parsedown/test/data/paragraph_list.html new file mode 100644 index 0000000..ced1c43 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/paragraph_list.html @@ -0,0 +1,12 @@ +

paragraph

+
    +
  • li
  • +
  • li
  • +
+

paragraph

+
    +
  • +

    li

    +
  • +
  • li
  • +
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/paragraph_list.md b/vendor/erusev/parsedown/test/data/paragraph_list.md new file mode 100644 index 0000000..b973908 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/paragraph_list.md @@ -0,0 +1,9 @@ +paragraph +- li +- li + +paragraph + + * li + + * li \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/reference_title.html b/vendor/erusev/parsedown/test/data/reference_title.html new file mode 100644 index 0000000..8f2be94 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/reference_title.html @@ -0,0 +1,2 @@ +

double quotes and single quotes and parentheses

+

[invalid title]: http://example.com example title

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/reference_title.md b/vendor/erusev/parsedown/test/data/reference_title.md new file mode 100644 index 0000000..43cb217 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/reference_title.md @@ -0,0 +1,6 @@ +[double quotes] and [single quotes] and [parentheses] + +[double quotes]: http://example.com "example title" +[single quotes]: http://example.com 'example title' +[parentheses]: http://example.com (example title) +[invalid title]: http://example.com example title \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/self-closing_html.html b/vendor/erusev/parsedown/test/data/self-closing_html.html new file mode 100644 index 0000000..4d072b4 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/self-closing_html.html @@ -0,0 +1,12 @@ +
+

paragraph

+
+

paragraph

+
+

paragraph

+
+

paragraph

+
+

paragraph

+
+

paragraph

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/self-closing_html.md b/vendor/erusev/parsedown/test/data/self-closing_html.md new file mode 100644 index 0000000..acb2032 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/self-closing_html.md @@ -0,0 +1,12 @@ +
+paragraph +
+paragraph +
+paragraph +
+paragraph +
+paragraph +
+paragraph \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/separated_nested_list.html b/vendor/erusev/parsedown/test/data/separated_nested_list.html new file mode 100644 index 0000000..80a5cae --- /dev/null +++ b/vendor/erusev/parsedown/test/data/separated_nested_list.html @@ -0,0 +1,9 @@ +
    +
  • +

    li

    +
      +
    • li
    • +
    • li
    • +
    +
  • +
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/separated_nested_list.md b/vendor/erusev/parsedown/test/data/separated_nested_list.md new file mode 100644 index 0000000..d7cd1af --- /dev/null +++ b/vendor/erusev/parsedown/test/data/separated_nested_list.md @@ -0,0 +1,4 @@ +- li + + - li + - li \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/setext_header.html b/vendor/erusev/parsedown/test/data/setext_header.html new file mode 100644 index 0000000..60aac08 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/setext_header.html @@ -0,0 +1,5 @@ +

h1

+

h2

+

single character

+

not a header

+
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/setext_header.md b/vendor/erusev/parsedown/test/data/setext_header.md new file mode 100644 index 0000000..c43b52c --- /dev/null +++ b/vendor/erusev/parsedown/test/data/setext_header.md @@ -0,0 +1,12 @@ +h1 +== + +h2 +-- + +single character +- + +not a header + +------------ \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/simple_blockquote.html b/vendor/erusev/parsedown/test/data/simple_blockquote.html new file mode 100644 index 0000000..8225d57 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/simple_blockquote.html @@ -0,0 +1,11 @@ +
+

quote

+
+

indented:

+
+

quote

+
+

no space after >:

+
+

quote

+
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/simple_blockquote.md b/vendor/erusev/parsedown/test/data/simple_blockquote.md new file mode 100644 index 0000000..22b6b11 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/simple_blockquote.md @@ -0,0 +1,7 @@ +> quote + +indented: + > quote + +no space after `>`: +>quote \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/simple_table.html b/vendor/erusev/parsedown/test/data/simple_table.html new file mode 100644 index 0000000..237d7ef --- /dev/null +++ b/vendor/erusev/parsedown/test/data/simple_table.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + +
header 1header 2
cell 1.1cell 1.2
cell 2.1cell 2.2
+
+ + + + + + + + + + + + + + + + + +
header 1header 2
cell 1.1cell 1.2
cell 2.1cell 2.2
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/simple_table.md b/vendor/erusev/parsedown/test/data/simple_table.md new file mode 100644 index 0000000..466d140 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/simple_table.md @@ -0,0 +1,11 @@ +header 1 | header 2 +-------- | -------- +cell 1.1 | cell 1.2 +cell 2.1 | cell 2.2 + +--- + +header 1 | header 2 +:------- | -------- +cell 1.1 | cell 1.2 +cell 2.1 | cell 2.2 \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/span-level_html.html b/vendor/erusev/parsedown/test/data/span-level_html.html new file mode 100644 index 0000000..f852a25 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/span-level_html.html @@ -0,0 +1,5 @@ +

an important link

+

broken
+line

+

inline tag at the beginning

+

http://example.com

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/span-level_html.md b/vendor/erusev/parsedown/test/data/span-level_html.md new file mode 100644 index 0000000..f221965 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/span-level_html.md @@ -0,0 +1,8 @@ +an important link + +broken
+line + +inline tag at the beginning + +http://example.com \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/sparse_dense_list.html b/vendor/erusev/parsedown/test/data/sparse_dense_list.html new file mode 100644 index 0000000..095bc73 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/sparse_dense_list.html @@ -0,0 +1,7 @@ +
    +
  • +

    li

    +
  • +
  • li
  • +
  • li
  • +
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/sparse_dense_list.md b/vendor/erusev/parsedown/test/data/sparse_dense_list.md new file mode 100644 index 0000000..5768422 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/sparse_dense_list.md @@ -0,0 +1,4 @@ +- li + +- li +- li \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/sparse_html.html b/vendor/erusev/parsedown/test/data/sparse_html.html new file mode 100644 index 0000000..9e89627 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/sparse_html.html @@ -0,0 +1,8 @@ +
+line 1 + +line 2 +line 3 + +line 4 +
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/sparse_html.md b/vendor/erusev/parsedown/test/data/sparse_html.md new file mode 100644 index 0000000..9e89627 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/sparse_html.md @@ -0,0 +1,8 @@ +
+line 1 + +line 2 +line 3 + +line 4 +
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/sparse_list.html b/vendor/erusev/parsedown/test/data/sparse_list.html new file mode 100644 index 0000000..452b2b8 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/sparse_list.html @@ -0,0 +1,15 @@ +
    +
  • +

    li

    +
  • +
  • li
  • +
+
+
    +
  • +

    li

    +
      +
    • indented li
    • +
    +
  • +
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/sparse_list.md b/vendor/erusev/parsedown/test/data/sparse_list.md new file mode 100644 index 0000000..362a35f --- /dev/null +++ b/vendor/erusev/parsedown/test/data/sparse_list.md @@ -0,0 +1,9 @@ +- li + +- li + +--- + +- li + + - indented li \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/special_characters.html b/vendor/erusev/parsedown/test/data/special_characters.html new file mode 100644 index 0000000..3b652c3 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/special_characters.html @@ -0,0 +1,6 @@ +

AT&T has an ampersand in their name

+

this & that

+

4 < 5 and 6 > 5

+

http://example.com/autolink?a=1&b=2

+

inline link

+

reference link

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/special_characters.md b/vendor/erusev/parsedown/test/data/special_characters.md new file mode 100644 index 0000000..111b03b --- /dev/null +++ b/vendor/erusev/parsedown/test/data/special_characters.md @@ -0,0 +1,13 @@ +AT&T has an ampersand in their name + +this & that + +4 < 5 and 6 > 5 + + + +[inline link](/script?a=1&b=2) + +[reference link][1] + +[1]: http://example.com/?a=1&b=2 \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/strikethrough.html b/vendor/erusev/parsedown/test/data/strikethrough.html new file mode 100644 index 0000000..2a9da98 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/strikethrough.html @@ -0,0 +1,3 @@ +

strikethrough

+

here's one followed by another one

+

~~ this ~~ is not one neither is ~this~

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/strikethrough.md b/vendor/erusev/parsedown/test/data/strikethrough.md new file mode 100644 index 0000000..d169144 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/strikethrough.md @@ -0,0 +1,5 @@ +~~strikethrough~~ + +here's ~~one~~ followed by ~~another one~~ + +~~ this ~~ is not one neither is ~this~ \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/strong_em.html b/vendor/erusev/parsedown/test/data/strong_em.html new file mode 100644 index 0000000..b709c99 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/strong_em.html @@ -0,0 +1,6 @@ +

em strong em

+

strong em em

+

em strong em em

+

em strong em

+

strong em em

+

em strong em em

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/strong_em.md b/vendor/erusev/parsedown/test/data/strong_em.md new file mode 100644 index 0000000..f2aa3c7 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/strong_em.md @@ -0,0 +1,11 @@ +*em **strong em*** + +***strong em** em* + +*em **strong em** em* + +_em __strong em___ + +___strong em__ em_ + +_em __strong em__ em_ \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/tab-indented_code_block.html b/vendor/erusev/parsedown/test/data/tab-indented_code_block.html new file mode 100644 index 0000000..7c140de --- /dev/null +++ b/vendor/erusev/parsedown/test/data/tab-indented_code_block.html @@ -0,0 +1,6 @@ +
<?php
+
+$message = 'Hello World!';
+echo $message;
+
+echo "following a blank line";
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/tab-indented_code_block.md b/vendor/erusev/parsedown/test/data/tab-indented_code_block.md new file mode 100644 index 0000000..a405a16 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/tab-indented_code_block.md @@ -0,0 +1,6 @@ + + + +header 1 +header 2 + + + + +cell 1.1 +cell 1.2 + + +| 2.1 +| 2.2 + + +\| 2.1 +link + + + \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/table_inline_markdown.md b/vendor/erusev/parsedown/test/data/table_inline_markdown.md new file mode 100644 index 0000000..2f3c620 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/table_inline_markdown.md @@ -0,0 +1,5 @@ +| _header_ 1 | header 2 | +| ------------ | ------------ | +| _cell_ 1.1 | ~~cell~~ 1.2 | +| `|` 2.1 | \| 2.2 | +| `\|` 2.1 | [link](/) | \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/text_reference.html b/vendor/erusev/parsedown/test/data/text_reference.html new file mode 100644 index 0000000..11e4d37 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/text_reference.html @@ -0,0 +1,8 @@ +

reference link

+

one with a semantic name

+

[one][404] with no definition

+

multiline +one defined on 2 lines

+

one with a mixed case label and an upper case definition

+

one with the a label on the next line

+

link

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/text_reference.md b/vendor/erusev/parsedown/test/data/text_reference.md new file mode 100644 index 0000000..1a66a5c --- /dev/null +++ b/vendor/erusev/parsedown/test/data/text_reference.md @@ -0,0 +1,21 @@ +[reference link][1] + +[1]: http://example.com + +[one][website] with a semantic name + +[website]: http://example.com + +[one][404] with no definition + +[multiline +one][website] defined on 2 lines + +[one][Label] with a mixed case label and an upper case definition + +[LABEL]: http://example.com + +[one] +[1] with the a label on the next line + +[`link`][website] \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/unordered_list.html b/vendor/erusev/parsedown/test/data/unordered_list.html new file mode 100644 index 0000000..cd95567 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/unordered_list.html @@ -0,0 +1,10 @@ +
    +
  • li
  • +
  • li
  • +
+

mixed markers:

+
    +
  • li
  • +
  • li
  • +
  • li
  • +
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/unordered_list.md b/vendor/erusev/parsedown/test/data/unordered_list.md new file mode 100644 index 0000000..cf62c99 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/unordered_list.md @@ -0,0 +1,8 @@ +- li +- li + +mixed markers: + +* li ++ li +- li \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/untidy_table.html b/vendor/erusev/parsedown/test/data/untidy_table.html new file mode 100644 index 0000000..88e1c2b --- /dev/null +++ b/vendor/erusev/parsedown/test/data/untidy_table.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + +
header 1header 2
cell 1.1cell 1.2
cell 2.1cell 2.2
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/untidy_table.md b/vendor/erusev/parsedown/test/data/untidy_table.md new file mode 100644 index 0000000..8524eb1 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/untidy_table.md @@ -0,0 +1,4 @@ +| header 1 | header 2 | +| ------------- | ----------- | +| cell 1.1 | cell 1.2 | +| cell 2.1 | cell 2.2 | \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/url_autolinking.html b/vendor/erusev/parsedown/test/data/url_autolinking.html new file mode 100644 index 0000000..58ca94c --- /dev/null +++ b/vendor/erusev/parsedown/test/data/url_autolinking.html @@ -0,0 +1,3 @@ +

an autolink http://example.com

+

inside of brackets [http://example.com], inside of braces {http://example.com}, inside of parentheses (http://example.com)

+

trailing slash http://example.com/ and http://example.com/path/

\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/url_autolinking.md b/vendor/erusev/parsedown/test/data/url_autolinking.md new file mode 100644 index 0000000..840f354 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/url_autolinking.md @@ -0,0 +1,5 @@ +an autolink http://example.com + +inside of brackets [http://example.com], inside of braces {http://example.com}, inside of parentheses (http://example.com) + +trailing slash http://example.com/ and http://example.com/path/ \ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/whitespace.html b/vendor/erusev/parsedown/test/data/whitespace.html new file mode 100644 index 0000000..f2dd7a0 --- /dev/null +++ b/vendor/erusev/parsedown/test/data/whitespace.html @@ -0,0 +1 @@ +
code
\ No newline at end of file diff --git a/vendor/erusev/parsedown/test/data/whitespace.md b/vendor/erusev/parsedown/test/data/whitespace.md new file mode 100644 index 0000000..4cf926a --- /dev/null +++ b/vendor/erusev/parsedown/test/data/whitespace.md @@ -0,0 +1,5 @@ + + + code + + \ No newline at end of file diff --git a/vendor/fguillot/picofeed/LICENSE b/vendor/fguillot/picofeed/LICENSE new file mode 100644 index 0000000..6a362bc --- /dev/null +++ b/vendor/fguillot/picofeed/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Frederic Guillot + +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: + +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/vendor/fguillot/picofeed/lib/PicoFeed/Base.php b/vendor/fguillot/picofeed/lib/PicoFeed/Base.php new file mode 100644 index 0000000..4be0985 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Base.php @@ -0,0 +1,34 @@ +config = $config ?: new Config(); + Logger::setTimezone($this->config->getTimezone()); + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Client/Client.php b/vendor/fguillot/picofeed/lib/PicoFeed/Client/Client.php new file mode 100644 index 0000000..d7f981b --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Client/Client.php @@ -0,0 +1,673 @@ +request_headers = $headers; + } + + /** + * Perform the HTTP request. + * + * @param string $url URL + * + * @return Client + */ + public function execute($url = '') + { + if ($url !== '') { + $this->url = $url; + } + + Logger::setMessage(get_called_class().' Fetch URL: '.$this->url); + Logger::setMessage(get_called_class().' Etag provided: '.$this->etag); + Logger::setMessage(get_called_class().' Last-Modified provided: '.$this->last_modified); + + $response = $this->doRequest(); + + $this->status_code = $response['status']; + $this->handleNotModifiedResponse($response); + $this->handleErrorResponse($response); + $this->handleNormalResponse($response); + + return $this; + } + + /** + * Handle not modified response. + * + * @param array $response Client response + */ + protected function handleNotModifiedResponse(array $response) + { + if ($response['status'] == 304) { + $this->is_modified = false; + } elseif ($response['status'] == 200) { + $this->is_modified = $this->hasBeenModified($response, $this->etag, $this->last_modified); + $this->etag = $this->getHeader($response, 'ETag'); + $this->last_modified = $this->getHeader($response, 'Last-Modified'); + } + + if ($this->is_modified === false) { + Logger::setMessage(get_called_class().' Resource not modified'); + } + } + + /** + * Handle Http Error codes + * + * @param array $response Client response + */ + protected function handleErrorResponse(array $response) + { + $status = $response['status']; + if ($status == 401) { + throw new UnauthorizedException('Wrong or missing credentials'); + } else if ($status == 403) { + throw new ForbiddenException('Not allowed to access resource'); + } else if ($status == 404) { + throw new InvalidUrlException('Resource not found'); + } + } + + /** + * Handle normal response. + * + * @param array $response Client response + */ + protected function handleNormalResponse(array $response) + { + if ($response['status'] == 200) { + $this->content = $response['body']; + $this->content_type = $this->findContentType($response); + $this->encoding = $this->findCharset(); + } + } + + /** + * Check if a request has been modified according to the parameters. + * + * @param array $response + * @param string $etag + * @param string $lastModified + * + * @return bool + */ + private function hasBeenModified($response, $etag, $lastModified) + { + $headers = array( + 'Etag' => $etag, + 'Last-Modified' => $lastModified, + ); + + // Compare the values for each header that is present + $presentCacheHeaderCount = 0; + foreach ($headers as $key => $value) { + if (isset($response['headers'][$key])) { + if ($response['headers'][$key] !== $value) { + return true; + } + ++$presentCacheHeaderCount; + } + } + + // If at least one header is present and the values match, the response + // was not modified + if ($presentCacheHeaderCount > 0) { + return false; + } + + return true; + } + + /** + * Find content type from response headers. + * + * @param array $response Client response + * + * @return string + */ + public function findContentType(array $response) + { + return strtolower($this->getHeader($response, 'Content-Type')); + } + + /** + * Find charset from response headers. + * + * @return string + */ + public function findCharset() + { + $result = explode('charset=', $this->content_type); + + return isset($result[1]) ? $result[1] : ''; + } + + /** + * Get header value from a client response. + * + * @param array $response Client response + * @param string $header Header name + * + * @return string + */ + public function getHeader(array $response, $header) + { + return isset($response['headers'][$header]) ? $response['headers'][$header] : ''; + } + + /** + * Set the Last-Modified HTTP header. + * + * @param string $last_modified Header value + * + * @return \PicoFeed\Client\Client + */ + public function setLastModified($last_modified) + { + $this->last_modified = $last_modified; + + return $this; + } + + /** + * Get the value of the Last-Modified HTTP header. + * + * @return string + */ + public function getLastModified() + { + return $this->last_modified; + } + + /** + * Set the value of the Etag HTTP header. + * + * @param string $etag Etag HTTP header value + * + * @return \PicoFeed\Client\Client + */ + public function setEtag($etag) + { + $this->etag = $etag; + + return $this; + } + + /** + * Get the Etag HTTP header value. + * + * @return string + */ + public function getEtag() + { + return $this->etag; + } + + /** + * Get the final url value. + * + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set the url. + * + * @return string + * @return \PicoFeed\Client\Client + */ + public function setUrl($url) + { + $this->url = $url; + + return $this; + } + + /** + * Get the HTTP response status code. + * + * @return int + */ + public function getStatusCode() + { + return $this->status_code; + } + + /** + * Get the body of the HTTP response. + * + * @return string + */ + public function getContent() + { + return $this->content; + } + + /** + * Get the content type value from HTTP headers. + * + * @return string + */ + public function getContentType() + { + return $this->content_type; + } + + /** + * Get the encoding value from HTTP headers. + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Return true if the remote resource has changed. + * + * @return bool + */ + public function isModified() + { + return $this->is_modified; + } + + /** + * return true if passthrough mode is enabled. + * + * @return bool + */ + public function isPassthroughEnabled() + { + return $this->passthrough; + } + + /** + * Set connection timeout. + * + * @param int $timeout Connection timeout + * + * @return \PicoFeed\Client\Client + */ + public function setTimeout($timeout) + { + $this->timeout = $timeout ?: $this->timeout; + + return $this; + } + + /** + * Set a custom user agent. + * + * @param string $user_agent User Agent + * + * @return \PicoFeed\Client\Client + */ + public function setUserAgent($user_agent) + { + $this->user_agent = $user_agent ?: $this->user_agent; + + return $this; + } + + /** + * Set the maximum number of HTTP redirections. + * + * @param int $max Maximum + * + * @return \PicoFeed\Client\Client + */ + public function setMaxRedirections($max) + { + $this->max_redirects = $max ?: $this->max_redirects; + + return $this; + } + + /** + * Set the maximum size of the HTTP body. + * + * @param int $max Maximum + * + * @return \PicoFeed\Client\Client + */ + public function setMaxBodySize($max) + { + $this->max_body_size = $max ?: $this->max_body_size; + + return $this; + } + + /** + * Set the proxy hostname. + * + * @param string $hostname Proxy hostname + * + * @return \PicoFeed\Client\Client + */ + public function setProxyHostname($hostname) + { + $this->proxy_hostname = $hostname ?: $this->proxy_hostname; + + return $this; + } + + /** + * Set the proxy port. + * + * @param int $port Proxy port + * + * @return \PicoFeed\Client\Client + */ + public function setProxyPort($port) + { + $this->proxy_port = $port ?: $this->proxy_port; + + return $this; + } + + /** + * Set the proxy username. + * + * @param string $username Proxy username + * + * @return \PicoFeed\Client\Client + */ + public function setProxyUsername($username) + { + $this->proxy_username = $username ?: $this->proxy_username; + + return $this; + } + + /** + * Set the proxy password. + * + * @param string $password Password + * + * @return \PicoFeed\Client\Client + */ + public function setProxyPassword($password) + { + $this->proxy_password = $password ?: $this->proxy_password; + + return $this; + } + + /** + * Set the username. + * + * @param string $username Basic Auth username + * + * @return \PicoFeed\Client\Client + */ + public function setUsername($username) + { + $this->username = $username ?: $this->username; + + return $this; + } + + /** + * Set the password. + * + * @param string $password Basic Auth Password + * + * @return \PicoFeed\Client\Client + */ + public function setPassword($password) + { + $this->password = $password ?: $this->password; + + return $this; + } + + /** + * Enable the passthrough mode. + * + * @return \PicoFeed\Client\Client + */ + public function enablePassthroughMode() + { + $this->passthrough = true; + + return $this; + } + + /** + * Disable the passthrough mode. + * + * @return \PicoFeed\Client\Client + */ + public function disablePassthroughMode() + { + $this->passthrough = false; + + return $this; + } + + /** + * Set config object. + * + * @param \PicoFeed\Config\Config $config Config instance + * + * @return \PicoFeed\Client\Client + */ + public function setConfig(Config $config) + { + if ($config !== null) { + $this->setTimeout($config->getClientTimeout()); + $this->setUserAgent($config->getClientUserAgent()); + $this->setMaxRedirections($config->getMaxRedirections()); + $this->setMaxBodySize($config->getMaxBodySize()); + $this->setProxyHostname($config->getProxyHostname()); + $this->setProxyPort($config->getProxyPort()); + $this->setProxyUsername($config->getProxyUsername()); + $this->setProxyPassword($config->getProxyPassword()); + } + + return $this; + } + + /** + * Return true if the HTTP status code is a redirection + * + * @access protected + * @param integer $code + * @return boolean + */ + public function isRedirection($code) + { + return $code == 301 || $code == 302 || $code == 303 || $code == 307; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Client/ClientException.php b/vendor/fguillot/picofeed/lib/PicoFeed/Client/ClientException.php new file mode 100644 index 0000000..b3a95c9 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Client/ClientException.php @@ -0,0 +1,14 @@ +body_length += $length; + + if ($this->body_length > $this->max_body_size) { + return -1; + } + + $this->body .= $buffer; + + return $length; + } + + /** + * cURL callback to read HTTP headers. + * + * @param resource $ch cURL handler + * @param string $buffer Header line + * + * @return int Length of the buffer + */ + public function readHeaders($ch, $buffer) + { + $length = strlen($buffer); + + if ($buffer === "\r\n" || $buffer === "\n") { + ++$this->response_headers_count; + } else { + if (!isset($this->response_headers[$this->response_headers_count])) { + $this->response_headers[$this->response_headers_count] = ''; + } + + $this->response_headers[$this->response_headers_count] .= $buffer; + } + + return $length; + } + + /** + * cURL callback to passthrough the HTTP body to the client. + * + * If the function return -1, curl stop to read the HTTP response + * + * @param resource $ch cURL handler + * @param string $buffer Chunk of data + * + * @return int Length of the buffer + */ + public function passthroughBody($ch, $buffer) + { + // do it only at the beginning of a transmission + if ($this->body_length === 0) { + list($status, $headers) = HttpHeaders::parse(explode("\n", $this->response_headers[$this->response_headers_count - 1])); + + if ($this->isRedirection($status)) { + return $this->handleRedirection($headers['Location']); + } + + header(':', true, $status); + + if (isset($headers['Content-Type'])) { + header('Content-Type:' .$headers['Content-Type']); + } + } + + $length = strlen($buffer); + $this->body_length += $length; + + echo $buffer; + + return $length; + } + + /** + * Prepare HTTP headers. + * + * @return string[] + */ + private function prepareHeaders() + { + $headers = array( + 'Connection: close', + ); + + if ($this->etag) { + $headers[] = 'If-None-Match: '.$this->etag; + } + + if ($this->last_modified) { + $headers[] = 'If-Modified-Since: '.$this->last_modified; + } + + $headers = array_merge($headers, $this->request_headers); + + return $headers; + } + + /** + * Prepare curl proxy context. + * + * @param resource $ch + * + * @return resource $ch + */ + private function prepareProxyContext($ch) + { + if ($this->proxy_hostname) { + Logger::setMessage(get_called_class().' Proxy: '.$this->proxy_hostname.':'.$this->proxy_port); + + curl_setopt($ch, CURLOPT_PROXYPORT, $this->proxy_port); + curl_setopt($ch, CURLOPT_PROXYTYPE, 'HTTP'); + curl_setopt($ch, CURLOPT_PROXY, $this->proxy_hostname); + + if ($this->proxy_username) { + Logger::setMessage(get_called_class().' Proxy credentials: Yes'); + curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->proxy_username.':'.$this->proxy_password); + } else { + Logger::setMessage(get_called_class().' Proxy credentials: No'); + } + } + + return $ch; + } + + /** + * Prepare curl auth context. + * + * @param resource $ch + * + * @return resource $ch + */ + private function prepareAuthContext($ch) + { + if ($this->username && $this->password) { + curl_setopt($ch, CURLOPT_USERPWD, $this->username.':'.$this->password); + } + + return $ch; + } + + /** + * Set write/header functions. + * + * @param resource $ch + * + * @return resource $ch + */ + private function prepareDownloadMode($ch) + { + $write_function = 'readBody'; + $header_function = 'readHeaders'; + + if ($this->isPassthroughEnabled()) { + $write_function = 'passthroughBody'; + } + + curl_setopt($ch, CURLOPT_WRITEFUNCTION, array($this, $write_function)); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($this, $header_function)); + + return $ch; + } + + /** + * Prepare curl context. + * + * @return resource + */ + private function prepareContext() + { + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $this->url); + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout); + curl_setopt($ch, CURLOPT_USERAGENT, $this->user_agent); + curl_setopt($ch, CURLOPT_HTTPHEADER, $this->prepareHeaders()); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); + curl_setopt($ch, CURLOPT_ENCODING, ''); + curl_setopt($ch, CURLOPT_COOKIEJAR, 'php://memory'); + curl_setopt($ch, CURLOPT_COOKIEFILE, 'php://memory'); + + // Disable SSLv3 by enforcing TLSv1.x for curl >= 7.34.0 and < 7.39.0. + // Versions prior to 7.34 and at least when compiled against openssl + // interpret this parameter as "limit to TLSv1.0" which fails for sites + // which enforce TLS 1.1+. + // Starting with curl 7.39.0 SSLv3 is disabled by default. + $version = curl_version(); + if ($version['version_number'] >= 467456 && $version['version_number'] < 468736) { + curl_setopt($ch, CURLOPT_SSLVERSION, 1); + } + + $ch = $this->prepareDownloadMode($ch); + $ch = $this->prepareProxyContext($ch); + $ch = $this->prepareAuthContext($ch); + + return $ch; + } + + /** + * Execute curl context. + */ + private function executeContext() + { + $ch = $this->prepareContext(); + curl_exec($ch); + + Logger::setMessage(get_called_class().' cURL total time: '.curl_getinfo($ch, CURLINFO_TOTAL_TIME)); + Logger::setMessage(get_called_class().' cURL dns lookup time: '.curl_getinfo($ch, CURLINFO_NAMELOOKUP_TIME)); + Logger::setMessage(get_called_class().' cURL connect time: '.curl_getinfo($ch, CURLINFO_CONNECT_TIME)); + Logger::setMessage(get_called_class().' cURL speed download: '.curl_getinfo($ch, CURLINFO_SPEED_DOWNLOAD)); + Logger::setMessage(get_called_class().' cURL effective url: '.curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)); + + $curl_errno = curl_errno($ch); + + if ($curl_errno) { + Logger::setMessage(get_called_class().' cURL error: '.curl_error($ch)); + curl_close($ch); + + $this->handleError($curl_errno); + } + + // Update the url if there where redirects + $this->url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL); + + curl_close($ch); + } + + /** + * Do the HTTP request. + * + * @return array HTTP response ['body' => ..., 'status' => ..., 'headers' => ...] + */ + public function doRequest() + { + $this->executeContext(); + + list($status, $headers) = HttpHeaders::parse(explode("\n", $this->response_headers[$this->response_headers_count - 1])); + + if ($this->isRedirection($status)) { + return $this->handleRedirection($headers['Location']); + } + + return array( + 'status' => $status, + 'body' => $this->body, + 'headers' => $headers, + ); + } + + /** + * Handle HTTP redirects + * + * @param string $location Redirected URL + * + * @return array + */ + private function handleRedirection($location) + { + $nb_redirects = 0; + $result = array(); + $this->url = Url::resolve($location, $this->url); + $this->body = ''; + $this->body_length = 0; + $this->response_headers = array(); + $this->response_headers_count = 0; + + while (true) { + ++$nb_redirects; + + if ($nb_redirects >= $this->max_redirects) { + throw new MaxRedirectException('Maximum number of redirections reached'); + } + + $result = $this->doRequest(); + + if ($this->isRedirection($result['status'])) { + $this->url = Url::resolve($result['headers']['Location'], $this->url); + $this->body = ''; + $this->body_length = 0; + $this->response_headers = array(); + $this->response_headers_count = 0; + } else { + break; + } + } + + return $result; + } + + /** + * Handle cURL errors (throw individual exceptions). + * + * We don't use constants because they are not necessary always available + * (depends of the version of libcurl linked to php) + * + * @see http://curl.haxx.se/libcurl/c/libcurl-errors.html + * + * @param int $errno cURL error code + */ + private function handleError($errno) + { + switch ($errno) { + case 78: // CURLE_REMOTE_FILE_NOT_FOUND + throw new InvalidUrlException('Resource not found', $errno); + case 6: // CURLE_COULDNT_RESOLVE_HOST + throw new InvalidUrlException('Unable to resolve hostname', $errno); + case 7: // CURLE_COULDNT_CONNECT + throw new InvalidUrlException('Unable to connect to the remote host', $errno); + case 23: // CURLE_WRITE_ERROR + throw new MaxSizeException('Maximum response size exceeded', $errno); + case 28: // CURLE_OPERATION_TIMEDOUT + throw new TimeoutException('Operation timeout', $errno); + case 35: // CURLE_SSL_CONNECT_ERROR + case 51: // CURLE_PEER_FAILED_VERIFICATION + case 58: // CURLE_SSL_CERTPROBLEM + case 60: // CURLE_SSL_CACERT + case 59: // CURLE_SSL_CIPHER + case 64: // CURLE_USE_SSL_FAILED + case 66: // CURLE_SSL_ENGINE_INITFAILED + case 77: // CURLE_SSL_CACERT_BADFILE + case 83: // CURLE_SSL_ISSUER_ERROR + $msg = 'Invalid SSL certificate caused by CURL error number ' . + $errno; + throw new InvalidCertificateException($msg, $errno); + case 47: // CURLE_TOO_MANY_REDIRECTS + throw new MaxRedirectException('Maximum number of redirections reached', $errno); + case 63: // CURLE_FILESIZE_EXCEEDED + throw new MaxSizeException('Maximum response size exceeded', $errno); + default: + throw new InvalidUrlException('Unable to fetch the URL', $errno); + } + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Client/ForbiddenException.php b/vendor/fguillot/picofeed/lib/PicoFeed/Client/ForbiddenException.php new file mode 100644 index 0000000..c226e95 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Client/ForbiddenException.php @@ -0,0 +1,10 @@ + $value) { + $this->headers[strtolower($key)] = $value; + } + } + + public function offsetGet($offset) + { + return $this->headers[strtolower($offset)]; + } + + public function offsetSet($offset, $value) + { + $this->headers[strtolower($offset)] = $value; + } + + public function offsetExists($offset) + { + return isset($this->headers[strtolower($offset)]); + } + + public function offsetUnset($offset) + { + unset($this->headers[strtolower($offset)]); + } + + /** + * Parse HTTP headers. + * + * @static + * + * @param array $lines List of headers + * + * @return array + */ + public static function parse(array $lines) + { + $status = 0; + $headers = array(); + + foreach ($lines as $line) { + if (strpos($line, 'HTTP/1') === 0) { + $headers = array(); + $status = (int) substr($line, 9, 3); + } elseif (strpos($line, ': ') !== false) { + list($name, $value) = explode(': ', $line); + if ($value) { + $headers[trim($name)] = trim($value); + } + } + } + + Logger::setMessage(get_called_class().' HTTP status code: '.$status); + + foreach ($headers as $name => $value) { + Logger::setMessage(get_called_class().' HTTP header: '.$name.' => '.$value); + } + + return array($status, new self($headers)); + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Client/InvalidCertificateException.php b/vendor/fguillot/picofeed/lib/PicoFeed/Client/InvalidCertificateException.php new file mode 100644 index 0000000..8d25d7e --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Client/InvalidCertificateException.php @@ -0,0 +1,12 @@ +user_agent, + ); + + // disable compression in passthrough mode. It could result in double + // compressed content which isn't decodeable by browsers + if (function_exists('gzdecode') && !$this->isPassthroughEnabled()) { + $headers[] = 'Accept-Encoding: gzip'; + } + + if ($this->etag) { + $headers[] = 'If-None-Match: '.$this->etag; + } + + if ($this->last_modified) { + $headers[] = 'If-Modified-Since: '.$this->last_modified; + } + + if ($this->proxy_username) { + $headers[] = 'Proxy-Authorization: Basic '.base64_encode($this->proxy_username.':'.$this->proxy_password); + } + + if ($this->username && $this->password) { + $headers[] = 'Authorization: Basic '.base64_encode($this->username.':'.$this->password); + } + + $headers = array_merge($headers, $this->request_headers); + + return $headers; + } + + /** + * Construct the final URL from location headers. + * + * @param array $headers List of HTTP response header + */ + private function setEffectiveUrl($headers) + { + foreach ($headers as $header) { + if (stripos($header, 'Location') === 0) { + list(, $value) = explode(': ', $header); + + $this->url = Url::resolve($value, $this->url); + } + } + } + + /** + * Prepare stream context. + * + * @return array + */ + private function prepareContext() + { + $context = array( + 'http' => array( + 'method' => 'GET', + 'protocol_version' => 1.1, + 'timeout' => $this->timeout, + 'max_redirects' => $this->max_redirects, + ), + ); + + if ($this->proxy_hostname) { + Logger::setMessage(get_called_class().' Proxy: '.$this->proxy_hostname.':'.$this->proxy_port); + + $context['http']['proxy'] = 'tcp://'.$this->proxy_hostname.':'.$this->proxy_port; + $context['http']['request_fulluri'] = true; + + if ($this->proxy_username) { + Logger::setMessage(get_called_class().' Proxy credentials: Yes'); + } else { + Logger::setMessage(get_called_class().' Proxy credentials: No'); + } + } + + $context['http']['header'] = implode("\r\n", $this->prepareHeaders()); + + return $context; + } + + /** + * Do the HTTP request. + * + * @return array HTTP response ['body' => ..., 'status' => ..., 'headers' => ...] + */ + public function doRequest() + { + $body = ''; + + // Create context + $context = stream_context_create($this->prepareContext()); + + // Make HTTP request + $stream = @fopen($this->url, 'r', false, $context); + if (!is_resource($stream)) { + throw new InvalidUrlException('Unable to establish a connection'); + } + + // Get HTTP headers response + $metadata = stream_get_meta_data($stream); + list($status, $headers) = HttpHeaders::parse($metadata['wrapper_data']); + + if ($this->isPassthroughEnabled()) { + header(':', true, $status); + + if (isset($headers['Content-Type'])) { + header('Content-Type: '.$headers['Content-Type']); + } + + fpassthru($stream); + } else { + // Get the entire body until the max size + $body = stream_get_contents($stream, $this->max_body_size + 1); + + // If the body size is too large abort everything + if (strlen($body) > $this->max_body_size) { + throw new MaxSizeException('Content size too large'); + } + + if ($metadata['timed_out']) { + throw new TimeoutException('Operation timeout'); + } + } + + fclose($stream); + + $this->setEffectiveUrl($metadata['wrapper_data']); + + return array( + 'status' => $status, + 'body' => $this->decodeBody($body, $headers), + 'headers' => $headers, + ); + } + + /** + * Decode body response according to the HTTP headers. + * + * @param string $body Raw body + * @param HttpHeaders $headers HTTP headers + * + * @return string + */ + public function decodeBody($body, HttpHeaders $headers) + { + if (isset($headers['Transfer-Encoding']) && $headers['Transfer-Encoding'] === 'chunked') { + $body = $this->decodeChunked($body); + } + + if (isset($headers['Content-Encoding']) && $headers['Content-Encoding'] === 'gzip') { + $body = gzdecode($body); + } + + return $body; + } + + /** + * Decode a chunked body. + * + * @param string $str Raw body + * + * @return string Decoded body + */ + public function decodeChunked($str) + { + for ($result = ''; !empty($str); $str = trim($str)) { + + // Get the chunk length + $pos = strpos($str, "\r\n"); + $len = hexdec(substr($str, 0, $pos)); + + // Append the chunk to the result + $result .= substr($str, $pos + 2, $len); + $str = substr($str, $pos + 2 + $len); + } + + return $result; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Client/TimeoutException.php b/vendor/fguillot/picofeed/lib/PicoFeed/Client/TimeoutException.php new file mode 100644 index 0000000..da98da1 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Client/TimeoutException.php @@ -0,0 +1,12 @@ +url = $url; + $this->components = parse_url($url) ?: array(); + + // Issue with PHP < 5.4.7 and protocol relative url + if (version_compare(PHP_VERSION, '5.4.7', '<') && $this->isProtocolRelative()) { + $pos = strpos($this->components['path'], '/', 2); + + if ($pos === false) { + $pos = strlen($this->components['path']); + } + + $this->components['host'] = substr($this->components['path'], 2, $pos - 2); + $this->components['path'] = substr($this->components['path'], $pos); + } + } + + /** + * Shortcut method to get an absolute url from relative url. + * + * @static + * + * @param mixed $item_url Unknown url (can be relative or not) + * @param mixed $website_url Website url + * + * @return string + */ + public static function resolve($item_url, $website_url) + { + $link = is_string($item_url) ? new self($item_url) : $item_url; + $website = is_string($website_url) ? new self($website_url) : $website_url; + + if ($link->isRelativeUrl()) { + if ($link->isRelativePath()) { + return $link->getAbsoluteUrl($website->getBaseUrl($website->getBasePath())); + } + + return $link->getAbsoluteUrl($website->getBaseUrl()); + } elseif ($link->isProtocolRelative()) { + $link->setScheme($website->getScheme()); + } + + return $link->getAbsoluteUrl(); + } + + /** + * Shortcut method to get a base url. + * + * @static + * + * @param string $url + * + * @return string + */ + public static function base($url) + { + $link = new self($url); + + return $link->getBaseUrl(); + } + + /** + * Get the base URL. + * + * @param string $suffix Add a suffix to the url + * + * @return string + */ + public function getBaseUrl($suffix = '') + { + return $this->hasHost() ? $this->getScheme('://').$this->getHost().$this->getPort(':').$suffix : ''; + } + + /** + * Get the absolute URL. + * + * @param string $base_url Use this url as base url + * + * @return string + */ + public function getAbsoluteUrl($base_url = '') + { + if ($base_url) { + $base = new self($base_url); + $url = $base->getAbsoluteUrl().substr($this->getFullPath(), 1); + } else { + $url = $this->hasHost() ? $this->getBaseUrl().$this->getFullPath() : ''; + } + + return $url; + } + + /** + * Return true if the url is relative. + * + * @return bool + */ + public function isRelativeUrl() + { + return !$this->hasScheme() && !$this->isProtocolRelative(); + } + + /** + * Return true if the path is relative. + * + * @return bool + */ + public function isRelativePath() + { + $path = $this->getPath(); + + return empty($path) || $path{0} + !== '/'; + } + + /** + * Filters the path of a URI. + * + * Imported from Guzzle library: https://github.com/guzzle/psr7/blob/master/src/Uri.php#L568-L582 + * + * @param $path + * + * @return string + */ + public function filterPath($path, $charUnreserved = 'a-zA-Z0-9_\-\.~', $charSubDelims = '!\$&\'\(\)\*\+,;=') + { + return preg_replace_callback( + '/(?:[^'.$charUnreserved.$charSubDelims.':@\/%]+|%(?![A-Fa-f0-9]{2}))/', + function (array $matches) { return rawurlencode($matches[0]); }, + $path + ); + } + + /** + * Get the path. + * + * @return string + */ + public function getPath() + { + return $this->filterPath(empty($this->components['path']) ? '' : $this->components['path']); + } + + /** + * Get the base path. + * + * @return string + */ + public function getBasePath() + { + $current_path = $this->getPath(); + + $path = $this->isRelativePath() ? '/' : ''; + $path .= substr($current_path, -1) === '/' ? $current_path : dirname($current_path); + + return preg_replace('/\\\\\/|\/\//', '/', $path.'/'); + } + + /** + * Get the full path (path + querystring + fragment). + * + * @return string + */ + public function getFullPath() + { + $path = $this->isRelativePath() ? '/' : ''; + $path .= $this->getPath(); + $path .= empty($this->components['query']) ? '' : '?'.$this->components['query']; + $path .= empty($this->components['fragment']) ? '' : '#'.$this->components['fragment']; + + return $path; + } + + /** + * Get the hostname. + * + * @return string + */ + public function getHost() + { + return empty($this->components['host']) ? '' : $this->components['host']; + } + + /** + * Return true if the url has a hostname. + * + * @return bool + */ + public function hasHost() + { + return !empty($this->components['host']); + } + + /** + * Get the scheme. + * + * @param string $suffix Suffix to add when there is a scheme + * + * @return string + */ + public function getScheme($suffix = '') + { + return ($this->hasScheme() ? $this->components['scheme'] : 'http').$suffix; + } + + /** + * Set the scheme. + * + * @param string $scheme Set a scheme + * + * @return string + */ + public function setScheme($scheme) + { + $this->components['scheme'] = $scheme; + } + + /** + * Return true if the url has a scheme. + * + * @return bool + */ + public function hasScheme() + { + return !empty($this->components['scheme']); + } + + /** + * Get the port. + * + * @param string $prefix Prefix to add when there is a port + * + * @return string + */ + public function getPort($prefix = '') + { + return $this->hasPort() ? $prefix.$this->components['port'] : ''; + } + + /** + * Return true if the url has a port. + * + * @return bool + */ + public function hasPort() + { + return !empty($this->components['port']); + } + + /** + * Return true if the url is protocol relative (start with //). + * + * @return bool + */ + public function isProtocolRelative() + { + return strpos($this->url, '//') === 0; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Config/Config.php b/vendor/fguillot/picofeed/lib/PicoFeed/Config/Config.php new file mode 100644 index 0000000..127968c --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Config/Config.php @@ -0,0 +1,96 @@ +container[$parameter] = $arguments[0]; + + return $this; + } elseif ($prefix === 'get') { + $default_value = isset($arguments[0]) ? $arguments[0] : null; + + return isset($this->container[$parameter]) ? $this->container[$parameter] : $default_value; + } + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Encoding/Encoding.php b/vendor/fguillot/picofeed/lib/PicoFeed/Encoding/Encoding.php new file mode 100644 index 0000000..fa0917e --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Encoding/Encoding.php @@ -0,0 +1,33 @@ + array('controls', 'src'), + 'video' => array('poster', 'controls', 'height', 'width', 'src'), + 'source' => array('src', 'type'), + 'dt' => array(), + 'dd' => array(), + 'dl' => array(), + 'table' => array(), + 'caption' => array(), + 'tr' => array(), + 'th' => array(), + 'td' => array(), + 'tbody' => array(), + 'thead' => array(), + 'h2' => array(), + 'h3' => array(), + 'h4' => array(), + 'h5' => array(), + 'h6' => array(), + 'strong' => array(), + 'em' => array(), + 'code' => array(), + 'pre' => array(), + 'blockquote' => array(), + 'p' => array(), + 'ul' => array(), + 'li' => array(), + 'ol' => array(), + 'br' => array(), + 'del' => array(), + 'a' => array('href'), + 'img' => array('src', 'title', 'alt'), + 'figure' => array(), + 'figcaption' => array(), + 'cite' => array(), + 'time' => array('datetime'), + 'abbr' => array('title'), + 'iframe' => array('width', 'height', 'frameborder', 'src', 'allowfullscreen'), + 'q' => array('cite'), + ); + + /** + * Scheme whitelist. + * + * For a complete list go to http://en.wikipedia.org/wiki/URI_scheme + * + * @var array + */ + private $scheme_whitelist = array( + 'bitcoin:', + 'callto:', + 'ed2k://', + 'facetime://', + 'feed:', + 'ftp://', + 'geo:', + 'git://', + 'http://', + 'https://', + 'irc://', + 'irc6://', + 'ircs://', + 'jabber:', + 'magnet:', + 'mailto:', + 'nntp://', + 'rtmp://', + 'sftp://', + 'sip:', + 'sips:', + 'skype:', + 'smb://', + 'sms:', + 'spotify:', + 'ssh:', + 'steam:', + 'svn://', + 'tel:', + ); + + /** + * Iframe source whitelist, everything else is ignored. + * + * @var array + */ + private $iframe_whitelist = array( + 'http://www.youtube.com', + 'https://www.youtube.com', + 'http://player.vimeo.com', + 'https://player.vimeo.com', + 'http://www.dailymotion.com', + 'https://www.dailymotion.com', + 'http://vk.com', + 'https://vk.com', + ); + + /** + * Blacklisted resources. + * + * @var array + */ + private $media_blacklist = array( + 'api.flattr.com', + 'feeds.feedburner.com', + 'share.feedsportal.com', + 'da.feedsportal.com', + 'rc.feedsportal.com', + 'rss.feedsportal.com', + 'res.feedsportal.com', + 'res1.feedsportal.com', + 'res2.feedsportal.com', + 'res3.feedsportal.com', + 'pi.feedsportal.com', + 'rss.nytimes.com', + 'feeds.wordpress.com', + 'stats.wordpress.com', + 'rss.cnn.com', + 'twitter.com/home?status=', + 'twitter.com/share', + 'twitter_icon_large.png', + 'www.facebook.com/sharer.php', + 'facebook_icon_large.png', + 'plus.google.com/share', + 'www.gstatic.com/images/icons/gplus-16.png', + 'www.gstatic.com/images/icons/gplus-32.png', + 'www.gstatic.com/images/icons/gplus-64.png', + ); + + /** + * Attributes used for external resources. + * + * @var array + */ + private $media_attributes = array( + 'src', + 'href', + 'poster', + ); + + /** + * Attributes that must be integer. + * + * @var array + */ + private $integer_attributes = array( + 'width', + 'height', + 'frameborder', + ); + + /** + * Mandatory attributes for specified tags. + * + * @var array + */ + private $required_attributes = array( + 'a' => array('href'), + 'img' => array('src'), + 'iframe' => array('src'), + 'audio' => array('src'), + 'source' => array('src'), + ); + + /** + * Add attributes to specified tags. + * + * @var array + */ + private $add_attributes = array( + 'a' => array('rel' => 'noreferrer', 'target' => '_blank'), + 'video' => array('controls' => 'true'), + ); + + /** + * List of filters to apply. + * + * @var array + */ + private $filters = array( + 'filterAllowedAttribute', + 'filterIntegerAttribute', + 'rewriteAbsoluteUrl', + 'filterIframeAttribute', + 'filterBlacklistResourceAttribute', + 'filterProtocolUrlAttribute', + 'rewriteImageProxyUrl', + 'secureIframeSrc', + 'removeYouTubeAutoplay', + ); + + /** + * Add attributes to specified tags. + * + * @var \PicoFeed\Client\Url + */ + private $website; + + /** + * Constructor. + * + * @param \PicoFeed\Client\Url $website Website url instance + */ + public function __construct(Url $website) + { + $this->website = $website; + } + + /** + * Apply filters to the attributes list. + * + * @param string $tag Tag name + * @param array $attributes Attributes dictionary + * + * @return array Filtered attributes + */ + public function filter($tag, array $attributes) + { + foreach ($attributes as $attribute => &$value) { + foreach ($this->filters as $filter) { + if (!$this->$filter($tag, $attribute, $value)) { + unset($attributes[$attribute]); + break; + } + } + } + + return $attributes; + } + + /** + * Return true if the value is allowed (remove not allowed attributes). + * + * @param string $tag Tag name + * @param string $attribute Attribute name + * @param string $value Attribute value + * + * @return bool + */ + public function filterAllowedAttribute($tag, $attribute, $value) + { + return isset($this->attribute_whitelist[$tag]) && in_array($attribute, $this->attribute_whitelist[$tag]); + } + + /** + * Return true if the value is not integer (remove attributes that should have an integer value). + * + * @param string $tag Tag name + * @param string $attribute Attribute name + * @param string $value Attribute value + * + * @return bool + */ + public function filterIntegerAttribute($tag, $attribute, $value) + { + if (in_array($attribute, $this->integer_attributes)) { + return ctype_digit($value); + } + + return true; + } + + /** + * Return true if the iframe source is allowed (remove not allowed iframe). + * + * @param string $tag Tag name + * @param string $attribute Attribute name + * @param string $value Attribute value + * + * @return bool + */ + public function filterIframeAttribute($tag, $attribute, $value) + { + if ($tag === 'iframe' && $attribute === 'src') { + foreach ($this->iframe_whitelist as $url) { + if (strpos($value, $url) === 0) { + return true; + } + } + + return false; + } + + return true; + } + + /** + * Return true if the resource is not blacklisted (remove blacklisted resource attributes). + * + * @param string $tag Tag name + * @param string $attribute Attribute name + * @param string $value Attribute value + * + * @return bool + */ + public function filterBlacklistResourceAttribute($tag, $attribute, $value) + { + if ($this->isResource($attribute) && $this->isBlacklistedMedia($value)) { + return false; + } + + return true; + } + + /** + * Convert all relative links to absolute url. + * + * @param string $tag Tag name + * @param string $attribute Attribute name + * @param string $value Attribute value + * + * @return bool + */ + public function rewriteAbsoluteUrl($tag, $attribute, &$value) + { + if ($this->isResource($attribute)) { + $value = Url::resolve($value, $this->website); + } + + return true; + } + + /** + * Turns iframes' src attribute from http to https to prevent + * mixed active content. + * + * @param string $tag Tag name + * @param array $attribute Atttributes name + * @param string $value Attribute value + * + * @return bool + */ + public function secureIframeSrc($tag, $attribute, &$value) + { + if ($tag === 'iframe' && $attribute === 'src' && strpos($value, 'http://') === 0) { + $value = substr_replace($value, 's', 4, 0); + } + + return true; + } + + /** + * Removes YouTube autoplay from iframes. + * + * @param string $tag Tag name + * @param array $attribute Atttributes name + * @param string $value Attribute value + * + * @return bool + */ + public function removeYouTubeAutoplay($tag, $attribute, &$value) + { + $regex = '%^(https://(?:www\.)?youtube.com/.*\?.*autoplay=)(1)(.*)%i'; + if ($tag === 'iframe' && $attribute === 'src' && preg_match($regex, $value)) { + $value = preg_replace($regex, '${1}0$3', $value); + } + + return true; + } + + /** + * Rewrite image url to use with a proxy. + * + * @param string $tag Tag name + * @param string $attribute Attribute name + * @param string $value Attribute value + * + * @return bool + */ + public function rewriteImageProxyUrl($tag, $attribute, &$value) + { + if ($tag === 'img' && $attribute === 'src' + && !($this->image_proxy_limit_protocol !== '' && stripos($value, $this->image_proxy_limit_protocol.':') !== 0)) { + if ($this->image_proxy_url) { + $value = sprintf($this->image_proxy_url, rawurlencode($value)); + } elseif (is_callable($this->image_proxy_callback)) { + $value = call_user_func($this->image_proxy_callback, $value); + } + } + + return true; + } + + /** + * Return true if the scheme is authorized. + * + * @param string $tag Tag name + * @param string $attribute Attribute name + * @param string $value Attribute value + * + * @return bool + */ + public function filterProtocolUrlAttribute($tag, $attribute, $value) + { + if ($this->isResource($attribute) && !$this->isAllowedProtocol($value)) { + return false; + } + + return true; + } + + /** + * Automatically add/override some attributes for specific tags. + * + * @param string $tag Tag name + * @param array $attributes Attributes list + * + * @return array + */ + public function addAttributes($tag, array $attributes) + { + if (isset($this->add_attributes[$tag])) { + $attributes += $this->add_attributes[$tag]; + } + + return $attributes; + } + + /** + * Return true if all required attributes are present. + * + * @param string $tag Tag name + * @param array $attributes Attributes list + * + * @return bool + */ + public function hasRequiredAttributes($tag, array $attributes) + { + if (isset($this->required_attributes[$tag])) { + foreach ($this->required_attributes[$tag] as $attribute) { + if (!isset($attributes[$attribute])) { + return false; + } + } + } + + return true; + } + + /** + * Check if an attribute name is an external resource. + * + * @param string $attribute Attribute name + * + * @return bool + */ + public function isResource($attribute) + { + return in_array($attribute, $this->media_attributes); + } + + /** + * Detect if the protocol is allowed or not. + * + * @param string $value Attribute value + * + * @return bool + */ + public function isAllowedProtocol($value) + { + foreach ($this->scheme_whitelist as $protocol) { + if (strpos($value, $protocol) === 0) { + return true; + } + } + + return false; + } + + /** + * Detect if an url is blacklisted. + * + * @param string $resource Attribute value (URL) + * + * @return bool + */ + public function isBlacklistedMedia($resource) + { + foreach ($this->media_blacklist as $name) { + if (strpos($resource, $name) !== false) { + return true; + } + } + + return false; + } + + /** + * Convert the attribute list to html. + * + * @param array $attributes Attributes + * + * @return string + */ + public function toHtml(array $attributes) + { + $html = array(); + + foreach ($attributes as $attribute => $value) { + $html[] = sprintf('%s="%s"', $attribute, Filter::escape($value)); + } + + return implode(' ', $html); + } + + /** + * Set whitelisted tags and attributes for each tag. + * + * @param array $values List of tags: ['video' => ['src', 'cover'], 'img' => ['src']] + * + * @return Attribute + */ + public function setWhitelistedAttributes(array $values) + { + $this->attribute_whitelist = $values ?: $this->attribute_whitelist; + + return $this; + } + + /** + * Set scheme whitelist. + * + * @param array $values List of scheme: ['http://', 'ftp://'] + * + * @return Attribute + */ + public function setSchemeWhitelist(array $values) + { + $this->scheme_whitelist = $values ?: $this->scheme_whitelist; + + return $this; + } + + /** + * Set media attributes (used to load external resources). + * + * @param array $values List of values: ['src', 'href'] + * + * @return Attribute + */ + public function setMediaAttributes(array $values) + { + $this->media_attributes = $values ?: $this->media_attributes; + + return $this; + } + + /** + * Set blacklisted external resources. + * + * @param array $values List of tags: ['http://google.com/', '...'] + * + * @return Attribute + */ + public function setMediaBlacklist(array $values) + { + $this->media_blacklist = $values ?: $this->media_blacklist; + + return $this; + } + + /** + * Set mandatory attributes for whitelisted tags. + * + * @param array $values List of tags: ['img' => 'src'] + * + * @return Attribute + */ + public function setRequiredAttributes(array $values) + { + $this->required_attributes = $values ?: $this->required_attributes; + + return $this; + } + + /** + * Set attributes to automatically to specific tags. + * + * @param array $values List of tags: ['a' => 'target="_blank"'] + * + * @return Attribute + */ + public function setAttributeOverrides(array $values) + { + $this->add_attributes = $values ?: $this->add_attributes; + + return $this; + } + + /** + * Set attributes that must be an integer. + * + * @param array $values List of tags: ['width', 'height'] + * + * @return Attribute + */ + public function setIntegerAttributes(array $values) + { + $this->integer_attributes = $values ?: $this->integer_attributes; + + return $this; + } + + /** + * Set allowed iframe resources. + * + * @param array $values List of tags: ['http://www.youtube.com'] + * + * @return Attribute + */ + public function setIframeWhitelist(array $values) + { + $this->iframe_whitelist = $values ?: $this->iframe_whitelist; + + return $this; + } + + /** + * Set image proxy URL. + * + * The original image url will be urlencoded + * + * @param string $url Proxy URL + * + * @return Attribute + */ + public function setImageProxyUrl($url) + { + $this->image_proxy_url = $url ?: $this->image_proxy_url; + + return $this; + } + + /** + * Set image proxy callback. + * + * @param \Closure $callback + * + * @return Attribute + */ + public function setImageProxyCallback($callback) + { + $this->image_proxy_callback = $callback ?: $this->image_proxy_callback; + + return $this; + } + + /** + * Set image proxy protocol restriction. + * + * @param string $value + * + * @return Attribute + */ + public function setImageProxyProtocol($value) + { + $this->image_proxy_limit_protocol = $value ?: $this->image_proxy_limit_protocol; + + return $this; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Filter/Filter.php b/vendor/fguillot/picofeed/lib/PicoFeed/Filter/Filter.php new file mode 100644 index 0000000..bae2aff --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Filter/Filter.php @@ -0,0 +1,155 @@ +]*>\s*~i', '', $data); + } + + /** + * Remove the XML tag from a document. + * + * @static + * + * @param string $data Input data + * + * @return string + */ + public static function stripXmlTag($data) + { + if (strpos($data, '') + 2)); + } + + do { + $pos = strpos($data, '') + 2)); + } + } while ($pos !== false && $pos < 200); + + return $data; + } + + /** + * Strip head tag from the HTML content. + * + * @static + * + * @param string $data Input data + * + * @return string + */ + public static function stripHeadTags($data) + { + return preg_replace('@]*?>.*?@siu', '', $data); + } + + /** + * Trim whitespace from the begining, the end and inside a string and don't break utf-8 string. + * + * @static + * + * @param string $value Raw data + * + * @return string Normalized data + */ + public static function stripWhiteSpace($value) + { + $value = str_replace("\r", ' ', $value); + $value = str_replace("\t", ' ', $value); + $value = str_replace("\n", ' ', $value); + // $value = preg_replace('/\s+/', ' ', $value); <= break utf-8 + return trim($value); + } + + /** + * Fixes before XML parsing. + * + * @static + * + * @param string $data Raw data + * + * @return string Normalized data + */ + public static function normalizeData($data) + { + $entities = array( + '/(&#)(\d+);/m', // decimal encoded + '/(&#x)([a-f0-9]+);/mi', // hex encoded + ); + + // strip invalid XML 1.0 characters which are encoded as entities + $data = preg_replace_callback($entities, function ($matches) { + $code_point = $matches[2]; + + // convert hex entity to decimal + if (strtolower($matches[1]) === '&#x') { + $code_point = hexdec($code_point); + } + + $code_point = (int) $code_point; + + // replace invalid characters + if ($code_point < 9 + || ($code_point > 10 && $code_point < 13) + || ($code_point > 13 && $code_point < 32) + || ($code_point > 55295 && $code_point < 57344) + || ($code_point > 65533 && $code_point < 65536) + || $code_point > 1114111 + ) { + return ''; + }; + + return $matches[0]; + }, $data); + + // strip every utf-8 character than isn't in the range of valid XML 1.0 characters + return (string) preg_replace('/[^\x{0009}\x{000A}\x{000D}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]/u', '', $data); + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Filter/Html.php b/vendor/fguillot/picofeed/lib/PicoFeed/Filter/Html.php new file mode 100644 index 0000000..0ccc192 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Filter/Html.php @@ -0,0 +1,243 @@ +config = new Config(); + $this->input = XmlParser::htmlToXml($html); + $this->output = ''; + $this->tag = new Tag($this->config); + $this->website = $website; + $this->attribute = new Attribute(new Url($website)); + } + + /** + * Set config object. + * + * @param \PicoFeed\Config\Config $config Config instance + * + * @return \PicoFeed\Filter\Html + */ + public function setConfig($config) + { + $this->config = $config; + + if ($this->config !== null) { + $this->attribute->setImageProxyCallback($this->config->getFilterImageProxyCallback()); + $this->attribute->setImageProxyUrl($this->config->getFilterImageProxyUrl()); + $this->attribute->setImageProxyProtocol($this->config->getFilterImageProxyProtocol()); + $this->attribute->setIframeWhitelist($this->config->getFilterIframeWhitelist(array())); + $this->attribute->setIntegerAttributes($this->config->getFilterIntegerAttributes(array())); + $this->attribute->setAttributeOverrides($this->config->getFilterAttributeOverrides(array())); + $this->attribute->setRequiredAttributes($this->config->getFilterRequiredAttributes(array())); + $this->attribute->setMediaBlacklist($this->config->getFilterMediaBlacklist(array())); + $this->attribute->setMediaAttributes($this->config->getFilterMediaAttributes(array())); + $this->attribute->setSchemeWhitelist($this->config->getFilterSchemeWhitelist(array())); + $this->attribute->setWhitelistedAttributes($this->config->getFilterWhitelistedTags(array())); + $this->tag->setWhitelistedTags(array_keys($this->config->getFilterWhitelistedTags(array()))); + } + + return $this; + } + + /** + * Run tags/attributes filtering. + * + * @return string + */ + public function execute() + { + $this->preFilter(); + + $parser = xml_parser_create(); + + xml_set_object($parser, $this); + xml_set_element_handler($parser, 'startTag', 'endTag'); + xml_set_character_data_handler($parser, 'dataTag'); + xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false); + xml_parse($parser, $this->input, true); + xml_parser_free($parser); + + $this->postFilter(); + + return $this->output; + } + + /** + * Called before XML parsing. + */ + public function preFilter() + { + $this->input = $this->tag->removeBlacklistedTags($this->input); + } + + /** + * Called after XML parsing. + */ + public function postFilter() + { + $this->output = $this->tag->removeEmptyTags($this->output); + $this->output = $this->filterRules($this->output); + $this->output = $this->tag->removeMultipleBreakTags($this->output); + $this->output = trim($this->output); + } + + /** + * Called after XML parsing. + * + * @param string $content the content that should be filtered + */ + public function filterRules($content) + { + // the constructor should require a config, then this if can be removed + if ($this->config === null) { + $config = new Config(); + } else { + $config = $this->config; + } + + $loader = new RuleLoader($config); + $rules = $loader->getRules($this->website); + + $url = new Url($this->website); + $sub_url = $url->getFullPath(); + + if (isset($rules['filter'])) { + foreach ($rules['filter'] as $pattern => $rule) { + if (preg_match($pattern, $sub_url)) { + foreach ($rule as $search => $replace) { + $content = preg_replace($search, $replace, $content); + } + } + } + } + + return $content; + } + + /** + * Parse opening tag. + * + * @param resource $parser XML parser + * @param string $tag Tag name + * @param array $attributes Tag attributes + */ + public function startTag($parser, $tag, array $attributes) + { + $this->empty = true; + + if ($this->tag->isAllowed($tag, $attributes)) { + $attributes = $this->attribute->filter($tag, $attributes); + + if ($this->attribute->hasRequiredAttributes($tag, $attributes)) { + $attributes = $this->attribute->addAttributes($tag, $attributes); + + $this->output .= $this->tag->openHtmlTag($tag, $this->attribute->toHtml($attributes)); + $this->empty = false; + } + } + + $this->empty_tags[] = $this->empty; + } + + /** + * Parse closing tag. + * + * @param resource $parser XML parser + * @param string $tag Tag name + */ + public function endTag($parser, $tag) + { + if (!array_pop($this->empty_tags) && $this->tag->isAllowedTag($tag)) { + $this->output .= $this->tag->closeHtmlTag($tag); + } + } + + /** + * Parse tag content. + * + * @param resource $parser XML parser + * @param string $content Tag content + */ + public function dataTag($parser, $content) + { + // Replace   with normal space + $content = str_replace("\xc2\xa0", ' ', $content); + $this->output .= Filter::escape($content); + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Filter/Tag.php b/vendor/fguillot/picofeed/lib/PicoFeed/Filter/Tag.php new file mode 100644 index 0000000..5fd8d6d --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Filter/Tag.php @@ -0,0 +1,215 @@ +isAllowedTag($tag) && !$this->isPixelTracker($tag, $attributes); + } + + /** + * Return the HTML opening tag. + * + * @param string $tag Tag name + * @param string $attributes Attributes converted in html + * + * @return string + */ + public function openHtmlTag($tag, $attributes = '') + { + return '<'.$tag.(empty($attributes) ? '' : ' '.$attributes).($this->isSelfClosingTag($tag) ? '/>' : '>'); + } + + /** + * Return the HTML closing tag. + * + * @param string $tag Tag name + * + * @return string + */ + public function closeHtmlTag($tag) + { + return $this->isSelfClosingTag($tag) ? '' : ''; + } + + /** + * Return true is the tag is self-closing. + * + * @param string $tag Tag name + * + * @return bool + */ + public function isSelfClosingTag($tag) + { + return $tag === 'br' || $tag === 'img'; + } + + /** + * Check if a tag is on the whitelist. + * + * @param string $tag Tag name + * + * @return bool + */ + public function isAllowedTag($tag) + { + return in_array($tag, array_merge( + $this->tag_whitelist, + array_keys($this->config->getFilterWhitelistedTags(array())) + )); + } + + /** + * Detect if an image tag is a pixel tracker. + * + * @param string $tag Tag name + * @param array $attributes Tag attributes + * + * @return bool + */ + public function isPixelTracker($tag, array $attributes) + { + return $tag === 'img' && + isset($attributes['height']) && isset($attributes['width']) && + $attributes['height'] == 1 && $attributes['width'] == 1; + } + + /** + * Remove script tags. + * + * @param string $data Input data + * + * @return string + */ + public function removeBlacklistedTags($data) + { + $dom = XmlParser::getDomDocument($data); + + if ($dom === false) { + return ''; + } + + $xpath = new DOMXpath($dom); + + $nodes = $xpath->query(implode(' | ', $this->tag_blacklist)); + + foreach ($nodes as $node) { + $node->parentNode->removeChild($node); + } + + return $dom->saveXML(); + } + + /** + * Remove empty tags. + * + * @param string $data Input data + * + * @return string + */ + public function removeEmptyTags($data) + { + return preg_replace('/<([^<\/>]*)>([\s]*?|(?R))<\/\1>/imsU', '', $data); + } + + /** + * Replace

by only one. + * + * @param string $data Input data + * + * @return string + */ + public function removeMultipleBreakTags($data) + { + return preg_replace("/(\s*)+/", '
', $data); + } + + /** + * Set whitelisted tags adn attributes for each tag. + * + * @param array $values List of tags: ['video' => ['src', 'cover'], 'img' => ['src']] + * + * @return Tag + */ + public function setWhitelistedTags(array $values) + { + $this->tag_whitelist = $values ?: $this->tag_whitelist; + + return $this; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Generator/ContentGeneratorInterface.php b/vendor/fguillot/picofeed/lib/PicoFeed/Generator/ContentGeneratorInterface.php new file mode 100644 index 0000000..5c2f205 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Generator/ContentGeneratorInterface.php @@ -0,0 +1,23 @@ +extensions as $extension) { + if (substr($item->getUrl(), - strlen($extension)) === $extension) { + $item->setContent(''.$item->getUrl().''); + return true; + } + } + + return false; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Generator/YoutubeContentGenerator.php b/vendor/fguillot/picofeed/lib/PicoFeed/Generator/YoutubeContentGenerator.php new file mode 100644 index 0000000..198090d --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Generator/YoutubeContentGenerator.php @@ -0,0 +1,67 @@ +hasNamespace('yt')) { + return $this->generateHtmlFromXml($item); + } + + return $this->generateHtmlFromUrl($item); + } + + /** + * Generate HTML + * + * @access public + * @param Item $item + * @return boolean + */ + private function generateHtmlFromXml(Item $item) + { + $videoId = $item->getTag('yt:videoId'); + + if (! empty($videoId)) { + $item->setContent(''); + return true; + } + + return false; + } + + /** + * Generate HTML from item URL + * + * @access public + * @param Item $item + * @return bool + */ + public function generateHtmlFromUrl(Item $item) + { + if (preg_match('/youtube\.com\/watch\?v=(.*)/', $item->getUrl(), $matches)) { + $item->setContent(''); + return true; + } + + return false; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Logging/Logger.php b/vendor/fguillot/picofeed/lib/PicoFeed/Logging/Logger.php new file mode 100644 index 0000000..caec463 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Logging/Logger.php @@ -0,0 +1,114 @@ +format('Y-m-d H:i:s').'] '.$message; + } + } + + /** + * Get all logged messages. + * + * @static + * + * @return array + */ + public static function getMessages() + { + return self::$messages; + } + + /** + * Remove all logged messages. + * + * @static + */ + public static function deleteMessages() + { + self::$messages = array(); + } + + /** + * Set a different timezone. + * + * @static + * + * @see http://php.net/manual/en/timezones.php + * + * @param string $timezone Timezone + */ + public static function setTimeZone($timezone) + { + self::$timezone = $timezone ?: self::$timezone; + } + + /** + * Get all messages serialized into a string. + * + * @static + * + * @return string + */ + public static function toString() + { + return implode(PHP_EOL, self::$messages).PHP_EOL; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Parser/Atom.php b/vendor/fguillot/picofeed/lib/PicoFeed/Parser/Atom.php new file mode 100644 index 0000000..19f841b --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Parser/Atom.php @@ -0,0 +1,364 @@ + 'http://www.w3.org/2005/Atom', + ); + + /** + * Get the path to the items XML tree. + * + * @param SimpleXMLElement $xml Feed xml + * + * @return SimpleXMLElement + */ + public function getItemsTree(SimpleXMLElement $xml) + { + return XmlParser::getXPathResult($xml, 'atom:entry', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'entry'); + } + + /** + * Find the feed url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedUrl(SimpleXMLElement $xml, Feed $feed) + { + $feed->setFeedUrl($this->getUrl($xml, 'self')); + } + + /** + * Find the site url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findSiteUrl(SimpleXMLElement $xml, Feed $feed) + { + $feed->setSiteUrl($this->getUrl($xml, 'alternate', true)); + } + + /** + * Find the feed description. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDescription(SimpleXMLElement $xml, Feed $feed) + { + $description = XmlParser::getXPathResult($xml, 'atom:subtitle', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'subtitle'); + + $feed->setDescription(XmlParser::getValue($description)); + } + + /** + * Find the feed logo url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLogo(SimpleXMLElement $xml, Feed $feed) + { + $logo = XmlParser::getXPathResult($xml, 'atom:logo', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'logo'); + + $feed->setLogo(XmlParser::getValue($logo)); + } + + /** + * Find the feed icon. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedIcon(SimpleXMLElement $xml, Feed $feed) + { + $icon = XmlParser::getXPathResult($xml, 'atom:icon', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'icon'); + + $feed->setIcon(XmlParser::getValue($icon)); + } + + /** + * Find the feed title. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedTitle(SimpleXMLElement $xml, Feed $feed) + { + $title = XmlParser::getXPathResult($xml, 'atom:title', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'title'); + + $feed->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $feed->getSiteUrl()); + } + + /** + * Find the feed language. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed) + { + $language = XmlParser::getXPathResult($xml, '*[not(self::atom:entry)]/@xml:lang', $this->namespaces) + ?: XmlParser::getXPathResult($xml, '@xml:lang'); + + $feed->setLanguage(XmlParser::getValue($language)); + } + + /** + * Find the feed id. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedId(SimpleXMLElement $xml, Feed $feed) + { + $id = XmlParser::getXPathResult($xml, 'atom:id', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'id'); + + $feed->setId(XmlParser::getValue($id)); + } + + /** + * Find the feed date. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDate(SimpleXMLElement $xml, Feed $feed) + { + $updated = XmlParser::getXPathResult($xml, 'atom:updated', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'updated'); + + $feed->setDate($this->getDateParser()->getDateTime(XmlParser::getValue($updated))); + } + + /** + * Find the item date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $published = XmlParser::getXPathResult($entry, 'atom:published', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'published'); + + $updated = XmlParser::getXPathResult($entry, 'atom:updated', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'updated'); + + $published = !empty($published) ? $this->getDateParser()->getDateTime((string) current($published)) : null; + $updated = !empty($updated) ? $this->getDateParser()->getDateTime((string) current($updated)) : null; + + if ($published === null && $updated === null) { + $item->setDate($feed->getDate()); // We use the feed date if there is no date for the item + } elseif ($published !== null && $updated !== null) { + $item->setDate(max($published, $updated)); // We use the most recent date between published and updated + } else { + $item->setDate($updated ?: $published); + } + } + + /** + * Find the item title. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + */ + public function findItemTitle(SimpleXMLElement $entry, Item $item) + { + $title = XmlParser::getXPathResult($entry, 'atom:title', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'title'); + + $item->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $item->getUrl()); + } + + /** + * Find the item author. + * + * @param SimpleXMLElement $xml Feed + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) + { + $author = XmlParser::getXPathResult($entry, 'atom:author/atom:name', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'author/name') + ?: XmlParser::getXPathResult($xml, 'atom:author/atom:name', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'author/name'); + + $item->setAuthor(XmlParser::getValue($author)); + } + + /** + * Find the item content. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemContent(SimpleXMLElement $entry, Item $item) + { + $item->setContent($this->getContent($entry)); + } + + /** + * Find the item URL. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemUrl(SimpleXMLElement $entry, Item $item) + { + $item->setUrl($this->getUrl($entry, 'alternate', true)); + } + + /** + * Genereate the item id. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $id = XmlParser::getXPathResult($entry, 'atom:id', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'id'); + + if (!empty($id)) { + $item->setId($this->generateId(XmlParser::getValue($id))); + } else { + $item->setId($this->generateId( + $item->getTitle(), $item->getUrl(), $item->getContent() + )); + } + } + + /** + * Find the item enclosure. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $enclosure = $this->findLink($entry, 'enclosure'); + + if ($enclosure) { + $item->setEnclosureUrl(Url::resolve((string) $enclosure['href'], $feed->getSiteUrl())); + $item->setEnclosureType((string) $enclosure['type']); + } + } + + /** + * Find the item language. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $language = XmlParser::getXPathResult($entry, './/@xml:lang'); + $item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage()); + } + + /** + * Get the URL from a link tag. + * + * @param SimpleXMLElement $xml XML tag + * @param string $rel Link relationship: alternate, enclosure, related, self, via + * + * @return string + */ + private function getUrl(SimpleXMLElement $xml, $rel, $fallback = false) + { + $link = $this->findLink($xml, $rel); + + if ($link) { + return (string) $link['href']; + } + + if ($fallback) { + $link = $this->findLink($xml, ''); + return $link ? (string) $link['href'] : ''; + } + + return ''; + } + + /** + * Get a link tag that match a relationship. + * + * @param SimpleXMLElement $xml XML tag + * @param string $rel Link relationship: alternate, enclosure, related, self, via + * + * @return SimpleXMLElement|null + */ + private function findLink(SimpleXMLElement $xml, $rel) + { + $links = XmlParser::getXPathResult($xml, 'atom:link', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'link'); + + foreach ($links as $link) { + if ($rel === (string) $link['rel']) { + return $link; + } + } + + return null; + } + + /** + * Get the entry content. + * + * @param SimpleXMLElement $entry XML Entry + * + * @return string + */ + private function getContent(SimpleXMLElement $entry) + { + $content = current( + XmlParser::getXPathResult($entry, 'atom:content', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'content') + ); + + if (!empty($content) && count($content->children())) { + $xml_string = ''; + + foreach ($content->children() as $child) { + $xml_string .= $child->asXML(); + } + + return $xml_string; + } elseif (trim((string) $content) !== '') { + return (string) $content; + } + + $summary = XmlParser::getXPathResult($entry, 'atom:summary', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'summary'); + + return (string) current($summary); + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Parser/DateParser.php b/vendor/fguillot/picofeed/lib/PicoFeed/Parser/DateParser.php new file mode 100644 index 0000000..4ad0078 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Parser/DateParser.php @@ -0,0 +1,126 @@ + length ]. + * + * @var array + */ + public $formats = array( + DATE_ATOM => null, + DATE_RSS => null, + DATE_COOKIE => null, + DATE_ISO8601 => null, + DATE_RFC822 => null, + DATE_RFC850 => null, + DATE_RFC1036 => null, + DATE_RFC1123 => null, + DATE_RFC2822 => null, + DATE_RFC3339 => null, + 'D, d M Y H:i:s' => 25, + 'D, d M Y h:i:s' => 25, + 'D M d Y H:i:s' => 24, + 'j M Y H:i:s' => 20, + 'Y-m-d H:i:s' => 19, + 'Y-m-d\TH:i:s' => 19, + 'd/m/Y H:i:s' => 19, + 'D, d M Y' => 16, + 'Y-m-d' => 10, + 'd-m-Y' => 10, + 'm-d-Y' => 10, + 'd.m.Y' => 10, + 'm.d.Y' => 10, + 'd/m/Y' => 10, + 'm/d/Y' => 10, + ); + + /** + * Try to parse all date format for broken feeds. + * + * @param string $value Original date format + * + * @return DateTime + */ + public function getDateTime($value) + { + $value = trim($value); + + foreach ($this->formats as $format => $length) { + $truncated_value = $value; + if ($length !== null) { + $truncated_value = substr($truncated_value, 0, $length); + } + + $date = $this->getValidDate($format, $truncated_value); + if ($date !== false) { + return $date; + } + } + + return $this->getCurrentDateTime(); + } + + /** + * Get a valid date from a given format. + * + * @param string $format Date format + * @param string $value Original date value + * + * @return DateTime|bool + */ + public function getValidDate($format, $value) + { + $date = DateTime::createFromFormat($format, $value, $this->getTimeZone()); + + if ($date !== false) { + $errors = DateTime::getLastErrors(); + + if ($errors['error_count'] === 0 && $errors['warning_count'] === 0) { + return $date; + } + } + + return false; + } + + /** + * Get the current datetime. + * + * @return DateTime + */ + public function getCurrentDateTime() + { + return new DateTime('now', $this->getTimeZone()); + } + + /** + * Get DateTimeZone instance + * + * @access public + * @return DateTimeZone + */ + public function getTimeZone() + { + return new DateTimeZone($this->config->getTimezone() ?: $this->timezone); + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Parser/Feed.php b/vendor/fguillot/picofeed/lib/PicoFeed/Parser/Feed.php new file mode 100644 index 0000000..7095728 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Parser/Feed.php @@ -0,0 +1,314 @@ +$property.PHP_EOL; + } + + $output .= 'Feed::date = '.$this->date->format(DATE_RFC822).PHP_EOL; + $output .= 'Feed::isRTL() = '.($this->isRTL() ? 'true' : 'false').PHP_EOL; + $output .= 'Feed::items = '.count($this->items).' items'.PHP_EOL; + + foreach ($this->items as $item) { + $output .= '----'.PHP_EOL; + $output .= $item; + } + + return $output; + } + + /** + * Get title. + */ + public function getTitle() + { + return $this->title; + } + + /** + * Get description. + */ + public function getDescription() + { + return $this->description; + } + + /** + * Get the logo url. + */ + public function getLogo() + { + return $this->logo; + } + + /** + * Get the icon url. + */ + public function getIcon() + { + return $this->icon; + } + + /** + * Get feed url. + */ + public function getFeedUrl() + { + return $this->feedUrl; + } + + /** + * Get site url. + */ + public function getSiteUrl() + { + return $this->siteUrl; + } + + /** + * Get date. + */ + public function getDate() + { + return $this->date; + } + + /** + * Get language. + */ + public function getLanguage() + { + return $this->language; + } + + /** + * Get id. + */ + public function getId() + { + return $this->id; + } + + /** + * Get feed items. + */ + public function getItems() + { + return $this->items; + } + + /** + * Return true if the feed is "Right to Left". + * + * @return bool + */ + public function isRTL() + { + return Parser::isLanguageRTL($this->language); + } + + /** + * Set feed items. + * + * @param Item[] $items + * @return Feed + */ + public function setItems(array $items) + { + $this->items = $items; + return $this; + } + + /** + * Set feed id. + * + * @param string $id + * @return Feed + */ + public function setId($id) + { + $this->id = $id; + return $this; + } + + /** + * Set feed title. + * + * @param string $title + * @return Feed + */ + public function setTitle($title) + { + $this->title = $title; + return $this; + } + + /** + * Set feed description. + * + * @param string $description + * @return Feed + */ + public function setDescription($description) + { + $this->description = $description; + return $this; + } + + /** + * Set feed url. + * + * @param string $feedUrl + * @return Feed + */ + public function setFeedUrl($feedUrl) + { + $this->feedUrl = $feedUrl; + return $this; + } + + /** + * Set feed website url. + * + * @param string $siteUrl + * @return Feed + */ + public function setSiteUrl($siteUrl) + { + $this->siteUrl = $siteUrl; + return $this; + } + + /** + * Set feed date. + * + * @param \DateTime $date + * @return Feed + */ + public function setDate($date) + { + $this->date = $date; + return $this; + } + + /** + * Set feed language. + * + * @param string $language + * @return Feed + */ + public function setLanguage($language) + { + $this->language = $language; + return $this; + } + + /** + * Set feed logo. + * + * @param string $logo + * @return Feed + */ + public function setLogo($logo) + { + $this->logo = $logo; + return $this; + } + + /** + * Set feed icon. + * + * @param string $icon + * @return Feed + */ + public function setIcon($icon) + { + $this->icon = $icon; + return $this; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Parser/Item.php b/vendor/fguillot/picofeed/lib/PicoFeed/Parser/Item.php new file mode 100644 index 0000000..a2a966c --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Parser/Item.php @@ -0,0 +1,415 @@ +namespaces); + } + + /** + * Get specific XML tag or attribute value. + * + * @param string $tag Tag name (examples: guid, media:content) + * @param string $attribute Tag attribute + * + * @return array|false Tag values or error + */ + public function getTag($tag, $attribute = '') + { + if ($attribute !== '') { + $attribute = '/@'.$attribute; + } + + $query = './/'.$tag.$attribute; + $elements = XmlParser::getXPathResult($this->xml, $query, $this->namespaces); + + if ($elements === false) { // xPath error + return false; + } + + return array_map(function ($element) { return (string) $element;}, $elements); + } + + /** + * Return item information. + * + * @return string + */ + public function __toString() + { + $output = ''; + + foreach (array('id', 'title', 'url', 'language', 'author', 'enclosureUrl', 'enclosureType') as $property) { + $output .= 'Item::'.$property.' = '.$this->$property.PHP_EOL; + } + + $output .= 'Item::date = '.$this->date->format(DATE_RFC822).PHP_EOL; + $output .= 'Item::isRTL() = '.($this->isRTL() ? 'true' : 'false').PHP_EOL; + $output .= 'Item::content = '.strlen($this->content).' bytes'.PHP_EOL; + + return $output; + } + + /** + * Get title. + * + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Get URL + * + * @access public + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set URL + * + * @access public + * @param string $url + * @return Item + */ + public function setUrl($url) + { + $this->url = $url; + return $this; + } + + /** + * Get id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Get date. + * + * @return \DateTime + */ + public function getDate() + { + return $this->date; + } + + /** + * Get content. + * + * @return string + */ + public function getContent() + { + return $this->content; + } + + /** + * Set content + * + * @access public + * @param string $value + * @return Item + */ + public function setContent($value) + { + $this->content = $value; + return $this; + } + + /** + * Get enclosure url. + * + * @return string + */ + public function getEnclosureUrl() + { + return $this->enclosureUrl; + } + + /** + * Get enclosure type. + * + * @return string + */ + public function getEnclosureType() + { + return $this->enclosureType; + } + + /** + * Get language. + * + * @return string + */ + public function getLanguage() + { + return $this->language; + } + + /** + * Get author. + * + * @return string + */ + public function getAuthor() + { + return $this->author; + } + + /** + * Return true if the item is "Right to Left". + * + * @return bool + */ + public function isRTL() + { + return Parser::isLanguageRTL($this->language); + } + + /** + * Set item id. + * + * @param string $id + * @return Item + */ + public function setId($id) + { + $this->id = $id; + return $this; + } + + /** + * Set item title. + * + * @param string $title + * @return Item + */ + public function setTitle($title) + { + $this->title = $title; + return $this; + } + + /** + * Set author. + * + * @param string $author + * @return Item + */ + public function setAuthor($author) + { + $this->author = $author; + return $this; + } + + /** + * Set item date. + * + * @param \DateTime $date + * @return Item + */ + public function setDate($date) + { + $this->date = $date; + return $this; + } + + /** + * Set enclosure url. + * + * @param string $enclosureUrl + * @return Item + */ + public function setEnclosureUrl($enclosureUrl) + { + $this->enclosureUrl = $enclosureUrl; + return $this; + } + + /** + * Set enclosure type. + * + * @param string $enclosureType + * @return Item + */ + public function setEnclosureType($enclosureType) + { + $this->enclosureType = $enclosureType; + return $this; + } + + /** + * Set item language. + * + * @param string $language + * @return Item + */ + public function setLanguage($language) + { + $this->language = $language; + return $this; + } + + /** + * Set raw XML. + * + * @param \SimpleXMLElement $xml + * @return Item + */ + public function setXml($xml) + { + $this->xml = $xml; + return $this; + } + + /** + * Get raw XML. + * + * @return \SimpleXMLElement + */ + public function getXml() + { + return $this->xml; + } + + /** + * Set XML namespaces. + * + * @param array $namespaces + * @return Item + */ + public function setNamespaces($namespaces) + { + $this->namespaces = $namespaces; + return $this; + } + + /** + * Get XML namespaces. + * + * @return array + */ + public function getNamespaces() + { + return $this->namespaces; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Parser/MalformedXmlException.php b/vendor/fguillot/picofeed/lib/PicoFeed/Parser/MalformedXmlException.php new file mode 100644 index 0000000..05b0387 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Parser/MalformedXmlException.php @@ -0,0 +1,12 @@ +fallback_url = $fallback_url; + $xml_encoding = XmlParser::getEncodingFromXmlTag($content); + + // Strip XML tag to avoid multiple encoding/decoding in the next XML processing + $this->content = Filter::stripXmlTag($content); + + // Encode everything in UTF-8 + Logger::setMessage(get_called_class().': HTTP Encoding "'.$http_encoding.'" ; XML Encoding "'.$xml_encoding.'"'); + $this->content = Encoding::convert($this->content, $xml_encoding ?: $http_encoding); + + $this->itemPostProcessor = new ItemPostProcessor($this->config); + $this->itemPostProcessor->register(new ContentGeneratorProcessor($this->config)); + $this->itemPostProcessor->register(new ContentFilterProcessor($this->config)); + } + + /** + * Parse the document. + * + * @return \PicoFeed\Parser\Feed + */ + public function execute() + { + Logger::setMessage(get_called_class().': begin parsing'); + + $xml = XmlParser::getSimpleXml($this->content); + + if ($xml === false) { + Logger::setMessage(get_called_class().': Applying XML workarounds'); + $this->content = Filter::normalizeData($this->content); + $xml = XmlParser::getSimpleXml($this->content); + + if ($xml === false) { + Logger::setMessage(get_called_class().': XML parsing error'); + Logger::setMessage(XmlParser::getErrors()); + throw new MalformedXmlException('XML parsing error'); + } + } + + $this->used_namespaces = $xml->getNamespaces(true); + $xml = $this->registerSupportedNamespaces($xml); + + $feed = new Feed(); + + $this->findFeedUrl($xml, $feed); + $this->checkFeedUrl($feed); + + $this->findSiteUrl($xml, $feed); + $this->checkSiteUrl($feed); + + $this->findFeedTitle($xml, $feed); + $this->findFeedDescription($xml, $feed); + $this->findFeedLanguage($xml, $feed); + $this->findFeedId($xml, $feed); + $this->findFeedDate($xml, $feed); + $this->findFeedLogo($xml, $feed); + $this->findFeedIcon($xml, $feed); + + foreach ($this->getItemsTree($xml) as $entry) { + $entry = $this->registerSupportedNamespaces($entry); + + $item = new Item(); + $item->xml = $entry; + $item->namespaces = $this->used_namespaces; + + $this->findItemAuthor($xml, $entry, $item); + + $this->findItemUrl($entry, $item); + $this->checkItemUrl($feed, $item); + + $this->findItemTitle($entry, $item); + $this->findItemContent($entry, $item); + + // Id generation can use the item url/title/content (order is important) + $this->findItemId($entry, $item, $feed); + $this->findItemDate($entry, $item, $feed); + $this->findItemEnclosure($entry, $item, $feed); + $this->findItemLanguage($entry, $item, $feed); + + $this->itemPostProcessor->execute($feed, $item); + $feed->items[] = $item; + } + + Logger::setMessage(get_called_class().PHP_EOL.$feed); + + return $feed; + } + + /** + * Check if the feed url is correct. + * + * @param Feed $feed Feed object + */ + public function checkFeedUrl(Feed $feed) + { + if ($feed->getFeedUrl() === '') { + $feed->feedUrl = $this->fallback_url; + } else { + $feed->feedUrl = Url::resolve($feed->getFeedUrl(), $this->fallback_url); + } + } + + /** + * Check if the site url is correct. + * + * @param Feed $feed Feed object + */ + public function checkSiteUrl(Feed $feed) + { + if ($feed->getSiteUrl() === '') { + $feed->siteUrl = Url::base($feed->getFeedUrl()); + } else { + $feed->siteUrl = Url::resolve($feed->getSiteUrl(), $this->fallback_url); + } + } + + /** + * Check if the item url is correct. + * + * @param Feed $feed Feed object + * @param Item $item Item object + */ + public function checkItemUrl(Feed $feed, Item $item) + { + $item->url = Url::resolve($item->getUrl(), $feed->getSiteUrl()); + } + + /** + * Get Item Post Processor instance + * + * @access public + * @return ItemPostProcessor + */ + public function getItemPostProcessor() + { + return $this->itemPostProcessor; + } + + /** + * Get DateParser instance + * + * @access public + * @return DateParser + */ + public function getDateParser() + { + if ($this->dateParser === null) { + return new DateParser($this->config); + } + + return $this->dateParser; + } + + /** + * Generate a unique id for an entry (hash all arguments). + * + * @return string + */ + public function generateId() + { + return hash($this->hash_algo, implode(func_get_args())); + } + + /** + * Return true if the given language is "Right to Left". + * + * @static + * + * @param string $language Language: fr-FR, en-US + * + * @return bool + */ + public static function isLanguageRTL($language) + { + $language = strtolower($language); + + $rtl_languages = array( + 'ar', // Arabic (ar-**) + 'fa', // Farsi (fa-**) + 'ur', // Urdu (ur-**) + 'ps', // Pashtu (ps-**) + 'syr', // Syriac (syr-**) + 'dv', // Divehi (dv-**) + 'he', // Hebrew (he-**) + 'yi', // Yiddish (yi-**) + ); + + foreach ($rtl_languages as $prefix) { + if (strpos($language, $prefix) === 0) { + return true; + } + } + + return false; + } + + /** + * Set Hash algorithm used for id generation. + * + * @param string $algo Algorithm name + * @return \PicoFeed\Parser\Parser + */ + public function setHashAlgo($algo) + { + $this->hash_algo = $algo ?: $this->hash_algo; + return $this; + } + + /** + * Set config object. + * + * @param \PicoFeed\Config\Config $config Config instance + * + * @return \PicoFeed\Parser\Parser + */ + public function setConfig($config) + { + $this->config = $config; + return $this; + } + + /** + * Enable the content grabber. + * + * @return \PicoFeed\Parser\Parser + */ + public function disableContentFiltering() + { + $this->itemPostProcessor->unregister('PicoFeed\Processor\ContentFilterProcessor'); + return $this; + } + + /** + * Enable the content grabber. + * + * @param bool $needsRuleFile true if only pages with rule files should be + * scraped + * @param null|\Closure $scraperCallback Callback function that gets called for each + * scraper execution + * + * @return \PicoFeed\Parser\Parser + */ + public function enableContentGrabber($needsRuleFile = false, $scraperCallback = null) + { + $processor = new ScraperProcessor($this->config); + + if ($needsRuleFile) { + $processor->getScraper()->disableCandidateParser(); + } + + if ($scraperCallback !== null) { + $processor->setExecutionCallback($scraperCallback); + } + + $this->itemPostProcessor->register($processor); + return $this; + } + + /** + * Set ignored URLs for the content grabber. + * + * @param array $urls URLs + * + * @return \PicoFeed\Parser\Parser + */ + public function setGrabberIgnoreUrls(array $urls) + { + $this->itemPostProcessor->getProcessor('PicoFeed\Processor\ScraperProcessor')->ignoreUrls($urls); + return $this; + } + + /** + * Register all supported namespaces to be used within an xpath query. + * + * @param SimpleXMLElement $xml Feed xml + * + * @return SimpleXMLElement + */ + public function registerSupportedNamespaces(SimpleXMLElement $xml) + { + foreach ($this->namespaces as $prefix => $ns) { + $xml->registerXPathNamespace($prefix, $ns); + } + + return $xml; + } + + /** + * Find the feed url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + abstract public function findFeedUrl(SimpleXMLElement $xml, Feed $feed); + + /** + * Find the site url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + abstract public function findSiteUrl(SimpleXMLElement $xml, Feed $feed); + + /** + * Find the feed title. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + abstract public function findFeedTitle(SimpleXMLElement $xml, Feed $feed); + + /** + * Find the feed description. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + abstract public function findFeedDescription(SimpleXMLElement $xml, Feed $feed); + + /** + * Find the feed language. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + abstract public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed); + + /** + * Find the feed id. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + abstract public function findFeedId(SimpleXMLElement $xml, Feed $feed); + + /** + * Find the feed date. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + abstract public function findFeedDate(SimpleXMLElement $xml, Feed $feed); + + /** + * Find the feed logo url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + abstract public function findFeedLogo(SimpleXMLElement $xml, Feed $feed); + + /** + * Find the feed icon. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + abstract public function findFeedIcon(SimpleXMLElement $xml, Feed $feed); + + /** + * Get the path to the items XML tree. + * + * @param SimpleXMLElement $xml Feed xml + * + * @return SimpleXMLElement + */ + abstract public function getItemsTree(SimpleXMLElement $xml); + + /** + * Find the item author. + * + * @param SimpleXMLElement $xml Feed + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + abstract public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item); + + /** + * Find the item URL. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + abstract public function findItemUrl(SimpleXMLElement $entry, Item $item); + + /** + * Find the item title. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + abstract public function findItemTitle(SimpleXMLElement $entry, Item $item); + + /** + * Genereate the item id. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + abstract public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed); + + /** + * Find the item date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + abstract public function findItemDate(SimpleXMLElement $entry, Item $item, Feed $feed); + + /** + * Find the item content. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + abstract public function findItemContent(SimpleXMLElement $entry, Item $item); + + /** + * Find the item enclosure. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + abstract public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed); + + /** + * Find the item language. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + abstract public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed); +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Parser/ParserException.php b/vendor/fguillot/picofeed/lib/PicoFeed/Parser/ParserException.php new file mode 100644 index 0000000..cfd102b --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Parser/ParserException.php @@ -0,0 +1,14 @@ + 'http://purl.org/rss/1.0/', + 'dc' => 'http://purl.org/dc/elements/1.1/', + 'content' => 'http://purl.org/rss/1.0/modules/content/', + 'feedburner' => 'http://rssnamespace.org/feedburner/ext/1.0', + ); + + /** + * Get the path to the items XML tree. + * + * @param SimpleXMLElement $xml Feed xml + * + * @return SimpleXMLElement + */ + public function getItemsTree(SimpleXMLElement $xml) + { + return XmlParser::getXPathResult($xml, 'rss:item', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'item') + ?: $xml->item; + } + + /** + * Find the feed url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedUrl(SimpleXMLElement $xml, Feed $feed) + { + $feed->setFeedUrl(''); + } + + /** + * Find the site url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findSiteUrl(SimpleXMLElement $xml, Feed $feed) + { + $value = XmlParser::getXPathResult($xml, 'rss:channel/rss:link', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/link') + ?: $xml->channel->link; + + $feed->setSiteUrl(XmlParser::getValue($value)); + } + + /** + * Find the feed description. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDescription(SimpleXMLElement $xml, Feed $feed) + { + $description = XmlParser::getXPathResult($xml, 'rss:channel/rss:description', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/description') + ?: $xml->channel->description; + + $feed->setDescription(XmlParser::getValue($description)); + } + + /** + * Find the feed logo url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLogo(SimpleXMLElement $xml, Feed $feed) + { + $logo = XmlParser::getXPathResult($xml, 'rss:image/rss:url', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'image/url'); + + $feed->setLogo(XmlParser::getValue($logo)); + } + + /** + * Find the feed icon. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedIcon(SimpleXMLElement $xml, Feed $feed) + { + $feed->setIcon(''); + } + + /** + * Find the feed title. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedTitle(SimpleXMLElement $xml, Feed $feed) + { + $title = XmlParser::getXPathResult($xml, 'rss:channel/rss:title', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/title') + ?: $xml->channel->title; + + $feed->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $feed->getSiteUrl()); + } + + /** + * Find the feed language. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed) + { + $language = XmlParser::getXPathResult($xml, 'rss:channel/dc:language', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/dc:language', $this->namespaces); + + $feed->setLanguage(XmlParser::getValue($language)); + } + + /** + * Find the feed id. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedId(SimpleXMLElement $xml, Feed $feed) + { + $feed->setId($feed->getFeedUrl() ?: $feed->getSiteUrl()); + } + + /** + * Find the feed date. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDate(SimpleXMLElement $xml, Feed $feed) + { + $date = XmlParser::getXPathResult($xml, 'rss:channel/dc:date', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/dc:date', $this->namespaces); + + $feed->setDate($this->getDateParser()->getDateTime(XmlParser::getValue($date))); + } + + /** + * Find the item date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $date = XmlParser::getXPathResult($entry, 'dc:date', $this->namespaces); + + $item->setDate(empty($date) ? $feed->getDate() : $this->getDateParser()->getDateTime(XmlParser::getValue($date))); + } + + /** + * Find the item title. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemTitle(SimpleXMLElement $entry, Item $item) + { + $title = XmlParser::getXPathResult($entry, 'rss:title', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'title') + ?: $entry->title; + + $item->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $item->getUrl()); + } + + /** + * Find the item author. + * + * @param SimpleXMLElement $xml Feed + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) + { + $author = XmlParser::getXPathResult($entry, 'dc:creator', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'rss:channel/dc:creator', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/dc:creator', $this->namespaces); + + $item->setAuthor(XmlParser::getValue($author)); + } + + /** + * Find the item content. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemContent(SimpleXMLElement $entry, Item $item) + { + $content = XmlParser::getXPathResult($entry, 'content:encoded', $this->namespaces); + + if (XmlParser::getValue($content) === '') { + $content = XmlParser::getXPathResult($entry, 'rss:description', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'description') + ?: $entry->description; + } + + $item->setContent(XmlParser::getValue($content)); + } + + /** + * Find the item URL. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemUrl(SimpleXMLElement $entry, Item $item) + { + $link = XmlParser::getXPathResult($entry, 'feedburner:origLink', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'rss:link', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'link') + ?: $entry->link; + + $item->setUrl(XmlParser::getValue($link)); + } + + /** + * Genereate the item id. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $item->setId($this->generateId( + $item->getTitle(), $item->getUrl(), $item->getContent() + )); + } + + /** + * Find the item enclosure. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed) + { + } + + /** + * Find the item language. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $language = XmlParser::getXPathResult($entry, 'dc:language', $this->namespaces); + + $item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage()); + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Parser/Rss20.php b/vendor/fguillot/picofeed/lib/PicoFeed/Parser/Rss20.php new file mode 100644 index 0000000..77f797d --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Parser/Rss20.php @@ -0,0 +1,289 @@ + 'http://purl.org/dc/elements/1.1/', + 'content' => 'http://purl.org/rss/1.0/modules/content/', + 'feedburner' => 'http://rssnamespace.org/feedburner/ext/1.0', + 'atom' => 'http://www.w3.org/2005/Atom', + ); + + /** + * Get the path to the items XML tree. + * + * @param SimpleXMLElement $xml Feed xml + * + * @return SimpleXMLElement + */ + public function getItemsTree(SimpleXMLElement $xml) + { + return XmlParser::getXPathResult($xml, 'channel/item'); + } + + /** + * Find the feed url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedUrl(SimpleXMLElement $xml, Feed $feed) + { + $feed->setFeedUrl(''); + } + + /** + * Find the site url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findSiteUrl(SimpleXMLElement $xml, Feed $feed) + { + $value = XmlParser::getXPathResult($xml, 'channel/link'); + $feed->setSiteUrl(XmlParser::getValue($value)); + } + + /** + * Find the feed description. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDescription(SimpleXMLElement $xml, Feed $feed) + { + $value = XmlParser::getXPathResult($xml, 'channel/description'); + $feed->setDescription(XmlParser::getValue($value)); + } + + /** + * Find the feed logo url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLogo(SimpleXMLElement $xml, Feed $feed) + { + $value = XmlParser::getXPathResult($xml, 'channel/image/url'); + $feed->setLogo(XmlParser::getValue($value)); + } + + /** + * Find the feed icon. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedIcon(SimpleXMLElement $xml, Feed $feed) + { + $feed->setIcon(''); + } + + /** + * Find the feed title. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedTitle(SimpleXMLElement $xml, Feed $feed) + { + $title = XmlParser::getXPathResult($xml, 'channel/title'); + $feed->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $feed->getSiteUrl()); + } + + /** + * Find the feed language. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed) + { + $value = XmlParser::getXPathResult($xml, 'channel/language'); + $feed->setLanguage(XmlParser::getValue($value)); + } + + /** + * Find the feed id. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedId(SimpleXMLElement $xml, Feed $feed) + { + $feed->setId($feed->getFeedUrl() ?: $feed->getSiteUrl()); + } + + /** + * Find the feed date. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDate(SimpleXMLElement $xml, Feed $feed) + { + $publish_date = XmlParser::getXPathResult($xml, 'channel/pubDate'); + $update_date = XmlParser::getXPathResult($xml, 'channel/lastBuildDate'); + + $published = !empty($publish_date) ? $this->getDateParser()->getDateTime(XmlParser::getValue($publish_date)) : null; + $updated = !empty($update_date) ? $this->getDateParser()->getDateTime(XmlParser::getValue($update_date)) : null; + + if ($published === null && $updated === null) { + $feed->setDate($this->getDateParser()->getCurrentDateTime()); // We use the current date if there is no date for the feed + } elseif ($published !== null && $updated !== null) { + $feed->setDate(max($published, $updated)); // We use the most recent date between published and updated + } else { + $feed->setDate($updated ?: $published); + } + } + + /** + * Find the item date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $date = XmlParser::getXPathResult($entry, 'pubDate'); + + $item->setDate(empty($date) ? $feed->getDate() : $this->getDateParser()->getDateTime(XmlParser::getValue($date))); + } + + /** + * Find the item title. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemTitle(SimpleXMLElement $entry, Item $item) + { + $value = XmlParser::getXPathResult($entry, 'title'); + $item->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($value)) ?: $item->getUrl()); + } + + /** + * Find the item author. + * + * @param SimpleXMLElement $xml Feed + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) + { + $value = XmlParser::getXPathResult($entry, 'dc:creator', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'author') + ?: XmlParser::getXPathResult($xml, 'channel/dc:creator', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/managingEditor'); + + $item->setAuthor(XmlParser::getValue($value)); + } + + /** + * Find the item content. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemContent(SimpleXMLElement $entry, Item $item) + { + $content = XmlParser::getXPathResult($entry, 'content:encoded', $this->namespaces); + + if (XmlParser::getValue($content) === '') { + $content = XmlParser::getXPathResult($entry, 'description'); + } + + $item->setContent(XmlParser::getValue($content)); + } + + /** + * Find the item URL. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemUrl(SimpleXMLElement $entry, Item $item) + { + $link = XmlParser::getXPathResult($entry, 'feedburner:origLink', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'link') + ?: XmlParser::getXPathResult($entry, 'atom:link/@href', $this->namespaces); + + if (!empty($link)) { + $item->setUrl(XmlParser::getValue($link)); + } else { + $link = XmlParser::getXPathResult($entry, 'guid'); + $link = XmlParser::getValue($link); + + if (filter_var($link, FILTER_VALIDATE_URL) !== false) { + $item->setUrl($link); + } + } + } + + /** + * Genereate the item id. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $id = XmlParser::getValue(XmlParser::getXPathResult($entry, 'guid')); + + if ($id) { + $item->setId($this->generateId($id)); + } else { + $item->setId($this->generateId( + $item->getTitle(), $item->getUrl(), $item->getContent() + )); + } + } + + /** + * Find the item enclosure. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed) + { + if (isset($entry->enclosure)) { + $type = XmlParser::getXPathResult($entry, 'enclosure/@type'); + $url = XmlParser::getXPathResult($entry, 'feedburner:origEnclosureLink', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'enclosure/@url'); + + $item->setEnclosureUrl(Url::resolve(XmlParser::getValue($url), $feed->getSiteUrl())); + $item->setEnclosureType(XmlParser::getValue($type)); + } + } + + /** + * Find the item language. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $language = XmlParser::getXPathResult($entry, 'dc:language', $this->namespaces); + $item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage()); + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Parser/Rss91.php b/vendor/fguillot/picofeed/lib/PicoFeed/Parser/Rss91.php new file mode 100644 index 0000000..fe195e8 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Parser/Rss91.php @@ -0,0 +1,12 @@ +childNodes->length === 0) { + return false; + } + + return $dom; + } + + /** + * Small wrapper around ZendXml to turn their exceptions into picoFeed + * exceptions + * + * @param $input the xml to load + * @param $dom pass in a dom document or use null/omit if simpleXml should + * be used + */ + private static function scan($input, $dom = null) + { + try { + return Security::scan($input, $dom); + } catch(\ZendXml\Exception\RuntimeException $e) { + throw new XmlEntityException($e->getMessage()); + } + } + + /** + * Load HTML document by using a DomDocument instance or return false on failure. + * + * @static + * @param string $input XML content + * @return \DOMDocument + */ + public static function getHtmlDocument($input) + { + $dom = new DomDocument(); + + if (empty($input)) { + return $dom; + } + + libxml_use_internal_errors(true); + + if (version_compare(PHP_VERSION, '5.4.0', '>=')) { + $dom->loadHTML($input, LIBXML_NONET); + } else { + $dom->loadHTML($input); + } + + return $dom; + } + + /** + * Convert a HTML document to XML. + * + * @static + * + * @param string $html HTML document + * + * @return string + */ + public static function htmlToXml($html) + { + $dom = self::getHtmlDocument(''.$html); + return $dom->saveXML($dom->getElementsByTagName('body')->item(0)); + } + + /** + * Get XML parser errors. + * + * @static + * @return string + */ + public static function getErrors() + { + $errors = array(); + + foreach (libxml_get_errors() as $error) { + $errors[] = sprintf('XML error: %s (Line: %d - Column: %d - Code: %d)', + $error->message, + $error->line, + $error->column, + $error->code + ); + } + + return implode(', ', $errors); + } + + /** + * Get the encoding from a xml tag. + * + * @static + * @param string $data Input data + * @return string + */ + public static function getEncodingFromXmlTag($data) + { + $encoding = ''; + + if (strpos($data, '')); + $data = str_replace("'", '"', $data); + + $p1 = strpos($data, 'encoding='); + $p2 = strpos($data, '"', $p1 + 10); + + if ($p1 !== false && $p2 !== false) { + $encoding = substr($data, $p1 + 10, $p2 - $p1 - 10); + $encoding = strtolower($encoding); + } + } + + return $encoding; + } + + /** + * Get the charset from a meta tag. + * + * @static + * @param string $data Input data + * @return string + */ + public static function getEncodingFromMetaTag($data) + { + $encoding = ''; + + if (preg_match('/;]+)/i', $data, $match) === 1) { + $encoding = strtolower($match[1]); + } + + return $encoding; + } + + /** + * Rewrite XPath query to use namespace-uri and local-name derived from prefix. + * + * @param string $query XPath query + * @param array $ns Prefix to namespace URI mapping + * @return string + */ + public static function replaceXPathPrefixWithNamespaceURI($query, array $ns) + { + return preg_replace_callback('/([A-Z0-9]+):([A-Z0-9]+)/iu', function ($matches) use ($ns) { + // don't try to map the special prefix XML + if (strtolower($matches[1]) === 'xml') { + return $matches[0]; + } + + return '*[namespace-uri()="'.$ns[$matches[1]].'" and local-name()="'.$matches[2].'"]'; + }, + $query); + } + + /** + * Get the result elements of a XPath query. + * + * @param \SimpleXMLElement $xml XML element + * @param string $query XPath query + * @param array $ns Prefix to namespace URI mapping + * @return \SimpleXMLElement[] + */ + public static function getXPathResult(SimpleXMLElement $xml, $query, array $ns = array()) + { + if (!empty($ns)) { + $query = static::replaceXPathPrefixWithNamespaceURI($query, $ns); + } + + return $xml->xpath($query); + } + + /** + * Get the first Xpath result or SimpleXMLElement value + * + * @static + * @access public + * @param mixed $value + * @return string + */ + public static function getValue($value) + { + $result = ''; + + if (is_array($value) && count($value) > 0) { + $result = (string) $value[0]; + } elseif (is_a($value, 'SimpleXMLElement')) { + return $result = (string) $value; + } + + return trim($result); + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/PicoFeedException.php b/vendor/fguillot/picofeed/lib/PicoFeed/PicoFeedException.php new file mode 100644 index 0000000..2de9e4b --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/PicoFeedException.php @@ -0,0 +1,14 @@ +config->getContentFiltering(true)) { + $filter = Filter::html($item->getContent(), $feed->getSiteUrl()); + $filter->setConfig($this->config); + $item->setContent($filter->execute()); + } else { + Logger::setMessage(get_called_class().': Content filtering disabled'); + } + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Processor/ContentGeneratorProcessor.php b/vendor/fguillot/picofeed/lib/PicoFeed/Processor/ContentGeneratorProcessor.php new file mode 100644 index 0000000..49adf9c --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Processor/ContentGeneratorProcessor.php @@ -0,0 +1,49 @@ +generators as $generator) { + $className = '\PicoFeed\Generator\\'.ucfirst($generator).'ContentGenerator'; + $object = new $className($this->config); + + if ($object->execute($item)) { + return true; + } + } + + return false; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Processor/ItemPostProcessor.php b/vendor/fguillot/picofeed/lib/PicoFeed/Processor/ItemPostProcessor.php new file mode 100644 index 0000000..97425bf --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Processor/ItemPostProcessor.php @@ -0,0 +1,96 @@ +processors as $processor) { + if ($processor->execute($feed, $item)) { + return true; + } + } + + return false; + } + + /** + * Register a new Item post-processor + * + * @access public + * @param ItemProcessorInterface $processor + * @return ItemPostProcessor + */ + public function register(ItemProcessorInterface $processor) + { + $this->processors[get_class($processor)] = $processor; + return $this; + } + + /** + * Remove Processor instance + * + * @access public + * @param string $class + * @return ItemPostProcessor + */ + public function unregister($class) + { + if (isset($this->processors[$class])) { + unset($this->processors[$class]); + } + + return $this; + } + + /** + * Checks wheather a specific processor is registered or not + * + * @access public + * @param string $class + * @return bool + */ + public function hasProcessor($class) + { + return isset($this->processors[$class]); + } + + /** + * Get Processor instance + * + * @access public + * @param string $class + * @return ItemProcessorInterface|null + */ + public function getProcessor($class) + { + return isset($this->processors[$class]) ? $this->processors[$class] : null; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Processor/ItemProcessorInterface.php b/vendor/fguillot/picofeed/lib/PicoFeed/Processor/ItemProcessorInterface.php new file mode 100644 index 0000000..5d53226 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Processor/ItemProcessorInterface.php @@ -0,0 +1,25 @@ +executionCallback = $executionCallback; + return $this; + } + + /** + * Execute Item Processor + * + * @access public + * @param Feed $feed + * @param Item $item + * @return bool + */ + public function execute(Feed $feed, Item $item) + { + if (!in_array($item->getUrl(), $this->ignoredUrls)) { + $scraper = $this->getScraper(); + $scraper->setUrl($item->getUrl()); + $scraper->execute(); + + if ($this->executionCallback && is_callable($this->executionCallback)) { + call_user_func($this->executionCallback, $feed, $item, $scraper); + } + + if ($scraper->hasRelevantContent()) { + $item->setContent($scraper->getFilteredContent()); + } + } + + return false; + } + + /** + * Ignore list of URLs + * + * @access public + * @param array $urls + * @return $this + */ + public function ignoreUrls(array $urls) + { + $this->ignoredUrls = $urls; + return $this; + } + + /** + * Returns Scraper instance + * + * @access public + * @return Scraper + */ + public function getScraper() + { + if ($this->scraper === null) { + $this->scraper = new Scraper($this->config); + } + + return $this->scraper; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Reader/Favicon.php b/vendor/fguillot/picofeed/lib/PicoFeed/Reader/Favicon.php new file mode 100644 index 0000000..09feb49 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Reader/Favicon.php @@ -0,0 +1,190 @@ +content; + } + + /** + * Get the icon file type (available only after the download). + * + * @return string + */ + public function getType() + { + foreach ($this->types as $type) { + if (strpos($this->content_type, $type) === 0) { + return $type; + } + } + + return 'image/x-icon'; + } + + /** + * Get data URI (http://en.wikipedia.org/wiki/Data_URI_scheme). + * + * @return string + */ + public function getDataUri() + { + if (empty($this->content)) { + return ''; + } + + return sprintf( + 'data:%s;base64,%s', + $this->getType(), + base64_encode($this->content) + ); + } + + /** + * Download and check if a resource exists. + * + * @param string $url URL + * + * @return \PicoFeed\Client Client instance + */ + public function download($url) + { + $client = Client::getInstance(); + $client->setConfig($this->config); + + Logger::setMessage(get_called_class().' Download => '.$url); + + try { + $client->execute($url); + } catch (ClientException $e) { + Logger::setMessage(get_called_class().' Download Failed => '.$e->getMessage()); + } + + return $client; + } + + /** + * Check if a remote file exists. + * + * @param string $url URL + * + * @return bool + */ + public function exists($url) + { + return $this->download($url)->getContent() !== ''; + } + + /** + * Get the icon link for a website. + * + * @param string $website_link URL + * @param string $favicon_link optional URL + * + * @return string + */ + public function find($website_link, $favicon_link = '') + { + $website = new Url($website_link); + + if ($favicon_link !== '') { + $icons = array($favicon_link); + } else { + $icons = $this->extract($this->download($website->getBaseUrl('/'))->getContent()); + $icons[] = $website->getBaseUrl('/favicon.ico'); + } + + foreach ($icons as $icon_link) { + $icon_link = Url::resolve($icon_link, $website); + $resource = $this->download($icon_link); + $this->content = $resource->getContent(); + $this->content_type = $resource->getContentType(); + + if ($this->content !== '') { + return $icon_link; + } elseif ($favicon_link !== '') { + return $this->find($website_link); + } + } + + return ''; + } + + /** + * Extract the icon links from the HTML. + * + * @param string $html HTML + * + * @return array + */ + public function extract($html) + { + $icons = array(); + + if (empty($html)) { + return $icons; + } + + $dom = XmlParser::getHtmlDocument($html); + + $xpath = new DOMXpath($dom); + $elements = $xpath->query('//link[@rel="icon" or @rel="shortcut icon" or @rel="icon shortcut"]'); + + for ($i = 0; $i < $elements->length; ++$i) { + $icons[] = $elements->item($i)->getAttribute('href'); + } + + return $icons; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Reader/Reader.php b/vendor/fguillot/picofeed/lib/PicoFeed/Reader/Reader.php new file mode 100644 index 0000000..7b26dea --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Reader/Reader.php @@ -0,0 +1,190 @@ + '//feed', + 'Rss20' => '//rss[@version="2.0"]', + 'Rss92' => '//rss[@version="0.92"]', + 'Rss91' => '//rss[@version="0.91"]', + 'Rss10' => '//rdf', + ); + + /** + * Download a feed (no discovery). + * + * @param string $url Feed url + * @param string $last_modified Last modified HTTP header + * @param string $etag Etag HTTP header + * @param string $username HTTP basic auth username + * @param string $password HTTP basic auth password + * + * @return \PicoFeed\Client\Client + */ + public function download($url, $last_modified = '', $etag = '', $username = '', $password = '') + { + $url = $this->prependScheme($url); + + return Client::getInstance() + ->setConfig($this->config) + ->setLastModified($last_modified) + ->setEtag($etag) + ->setUsername($username) + ->setPassword($password) + ->execute($url); + } + + /** + * Discover and download a feed. + * + * @param string $url Feed or website url + * @param string $last_modified Last modified HTTP header + * @param string $etag Etag HTTP header + * @param string $username HTTP basic auth username + * @param string $password HTTP basic auth password + * + * @return \PicoFeed\Client\Client + */ + public function discover($url, $last_modified = '', $etag = '', $username = '', $password = '') + { + $client = $this->download($url, $last_modified, $etag, $username, $password); + + // It's already a feed or the feed was not modified + if (!$client->isModified() || $this->detectFormat($client->getContent())) { + return $client; + } + + // Try to find a subscription + $links = $this->find($client->getUrl(), $client->getContent()); + + if (empty($links)) { + throw new SubscriptionNotFoundException('Unable to find a subscription'); + } + + return $this->download($links[0], $last_modified, $etag, $username, $password); + } + + /** + * Find feed urls inside a HTML document. + * + * @param string $url Website url + * @param string $html HTML content + * + * @return array List of feed links + */ + public function find($url, $html) + { + Logger::setMessage(get_called_class().': Try to discover subscriptions'); + + $dom = XmlParser::getHtmlDocument($html); + $xpath = new DOMXPath($dom); + $links = array(); + + $queries = array( + '//link[@type="application/rss+xml"]', + '//link[@type="application/atom+xml"]', + ); + + foreach ($queries as $query) { + $nodes = $xpath->query($query); + + foreach ($nodes as $node) { + $link = $node->getAttribute('href'); + + if (!empty($link)) { + $feedUrl = new Url($link); + $siteUrl = new Url($url); + + $links[] = $feedUrl->getAbsoluteUrl($feedUrl->isRelativeUrl() ? $siteUrl->getBaseUrl() : ''); + } + } + } + + Logger::setMessage(get_called_class().': '.implode(', ', $links)); + + return $links; + } + + /** + * Get a parser instance. + * + * @param string $url Site url + * @param string $content Feed content + * @param string $encoding HTTP encoding + * + * @return \PicoFeed\Parser\Parser + */ + public function getParser($url, $content, $encoding) + { + $format = $this->detectFormat($content); + + if (empty($format)) { + throw new UnsupportedFeedFormatException('Unable to detect feed format'); + } + + $className = '\PicoFeed\Parser\\'.$format; + + $parser = new $className($content, $encoding, $url); + $parser->setHashAlgo($this->config->getParserHashAlgo()); + $parser->setConfig($this->config); + + return $parser; + } + + /** + * Detect the feed format. + * + * @param string $content Feed content + * + * @return string + */ + public function detectFormat($content) + { + $dom = XmlParser::getHtmlDocument($content); + $xpath = new DOMXPath($dom); + + foreach ($this->formats as $parser_name => $query) { + $nodes = $xpath->query($query); + + if ($nodes->length === 1) { + return $parser_name; + } + } + + return ''; + } + + /** + * Add the prefix "http://" if the end-user just enter a domain name. + * + * @param string $url Url + * @retunr string + */ + public function prependScheme($url) + { + if (!preg_match('%^https?://%', $url)) { + $url = 'http://'.$url; + } + + return $url; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Reader/ReaderException.php b/vendor/fguillot/picofeed/lib/PicoFeed/Reader/ReaderException.php new file mode 100644 index 0000000..4f03dbe --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Reader/ReaderException.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://combat.blog.lemonde.fr/2013/08/31/teddy-riner-le-rookie-devenu-rambo/#xtor=RSS-3208', + 'body' => array( + '//div[@class="entry-content"]', + ), + 'strip' => array( + '//*[contains(@class, "fb-like") or contains(@class, "social")]' + ), + ) + ) +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.blogs.nytimes.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.blogs.nytimes.com.php new file mode 100644 index 0000000..ee641b0 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.blogs.nytimes.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'title' => '//header/h1', + 'test_url' => 'http://bits.blogs.nytimes.com/2012/01/16/wikipedia-plans-to-go-dark-on-wednesday-to-protest-sopa/', + 'body' => array( + '//div[@class="postContent"]', + ), + 'strip' => array( + '//*[@class="shareToolsBox"]', + ), + ) + ) +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.igen.fr.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.igen.fr.php new file mode 100644 index 0000000..f2028f4 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.igen.fr.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.igen.fr/ailleurs/2014/05/nvidia-va-delaisser-les-smartphones-grand-public-86031', + 'body' => array( + '//div[contains(@class, "field-name-body")]' + ), + 'strip' => array( + ), + ) + ) +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.nytimes.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.nytimes.com.php new file mode 100644 index 0000000..ed27bb5 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.nytimes.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nytimes.com/2011/05/15/world/middleeast/15prince.html', + 'body' => array( + '//div[@class="articleBody"]', + ), + ) + ) +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.over-blog.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.over-blog.com.php new file mode 100644 index 0000000..cc5d83c --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.over-blog.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://eliascarpe.over-blog.com/2015/12/re-upload-projets-d-avenir.html', + 'body' => array( + '//div[contains(concat(" ", normalize-space(@class), " "), " ob-section ")]', + ), + ) + ) +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.phoronix.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.phoronix.com.php new file mode 100644 index 0000000..867faa4 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.phoronix.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.phoronix.com/scan.php?page=article&item=amazon_ec2_bare&num=1', + 'body' => array( + '//div[@class="content"]', + ), + 'strip' => array() + ) + ) +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.slate.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.slate.com.php new file mode 100644 index 0000000..a795bca --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.slate.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.slate.com/articles/business/moneybox/2013/08/microsoft_ceo_steve_ballmer_retires_a_firsthand_account_of_the_company_s.html', + 'body' => array( + '//div[@class="sl-art-body"]', + ), + 'strip' => array( + '//*[contains(@class, "social") or contains(@class, "comments") or contains(@class, "sl-article-floatin-tools") or contains(@class, "sl-art-pag")]', + '//*[@id="mys_slate_logged_in"]', + '//*[@id="sl_article_tools_myslate_bottom"]', + '//*[@id="mys_myslate"]', + '//*[@class="sl-viral-container"]', + '//*[@class="sl-art-creds-cntr"]', + '//*[@class="sl-art-ad-midflex"]', + ) + ) + ) +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.theguardian.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.theguardian.com.php new file mode 100644 index 0000000..e0d6f3f --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.theguardian.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.theguardian.com/sustainable-business/2015/feb/02/2015-hyper-transparency-global-business', + 'body' => array( + '//div[contains(@class, "content__main-column--article")]', + ), + 'strip' => array( + '//div[contains(@class, "meta-container")]', + ), + ) + ) +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.wikipedia.org.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.wikipedia.org.php new file mode 100644 index 0000000..7b8f76e --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.wikipedia.org.php @@ -0,0 +1,29 @@ + array( + '%.*%' => array( + 'test_url' => 'https://en.wikipedia.org/wiki/Grace_Hopper', + 'body' => array( + '//div[@id="bodyContent"]', + ), + 'strip' => array( + "//div[@id='toc']", + "//div[@id='catlinks']", + "//div[@id='jump-to-nav']", + "//div[@class='thumbcaption']//div[@class='magnify']", + "//table[@class='navbox']", + "//table[contains(@class, 'infobox')]", + "//div[@class='dablink']", + "//div[@id='contentSub']", + "//div[@id='siteSub']", + "//table[@id='persondata']", + "//table[contains(@class, 'metadata')]", + "//*[contains(@class, 'noprint')]", + "//*[contains(@class, 'printfooter')]", + "//*[contains(@class, 'editsection')]", + "//*[contains(@class, 'error')]", + "//span[@title='pronunciation:']", + ), + ) + ) +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.wired.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.wired.com.php new file mode 100644 index 0000000..b6a1b96 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.wired.com.php @@ -0,0 +1,31 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.wired.com/gamelife/2013/09/ouya-free-the-games/', + 'body' => array( + '//div[@data-js="gallerySlides"]', + '//article', + ), + 'strip' => array( + '//*[@id="linker_widget"]', + '//*[@class="credit"]', + '//div[@data-js="slideCount"]', + '//*[contains(@class="visually-hidden")]', + '//*[@data-slide-number="_endslate"]', + '//*[@id="related"]', + '//*[contains(@class, "bio")]', + '//*[contains(@class, "entry-footer")]', + '//*[contains(@class, "mobify_backtotop_link")]', + '//*[contains(@class, "gallery-navigation")]', + '//*[contains(@class, "gallery-thumbnail")]', + '//img[contains(@src, "1x1")]', + '//a[contains(@href, "creativecommons")]', + '//a[@href="#start-of-content"]', + '//ul[@id="article-tags"]', + ), + ) + ) +); + + diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.wsj.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.wsj.com.php new file mode 100644 index 0000000..f6e6cc1 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/.wsj.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://online.wsj.com/article/SB10001424127887324108204579023143974408428.html', + 'body' => array( + '//div[@class="articlePage"]', + ), + 'strip' => array( + '//*[@id="articleThumbnail_2"]', + '//*[@class="socialByline"]', + ) + ) + ) +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/01net.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/01net.com.php new file mode 100644 index 0000000..2f11fd0 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/01net.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.01net.com/editorial/624550/twitter-rachete-madbits-un-specialiste-francais-de-lanalyse-dimages/', + 'body' => array( + '//div[@class="article_ventre_box"]', + ), + 'strip' => array( + '//link', + '//*[contains(@class, "article_navigation")]', + '//h1', + '//*[contains(@class, "article_toolbarMain")]', + '//*[contains(@class, "article_imagehaute_box")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/abstrusegoose.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/abstrusegoose.com.php new file mode 100644 index 0000000..412d6a0 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/abstrusegoose.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%alt="(.+)" title="(.+)" */>%' => '/>
$1
$2', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/alainonline.net.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/alainonline.net.php new file mode 100644 index 0000000..48a571a --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/alainonline.net.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.alainonline.net/news_details.php?lang=arabic&sid=18907', + 'body' => array( + '//div[@class="news_details"]', + ), + 'strip' => array( + '//div[@class="news_details"]/div/div[last()]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/aljazeera.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/aljazeera.com.php new file mode 100644 index 0000000..81c0086 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/aljazeera.com.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.aljazeera.com/news/2015/09/xi-jinping-seattle-china-150922230118373.html', + 'body' => array( + '//figure[@class="article-content"]', + '//div[@class="article-body"]', + ), + 'strip' => array( + '//h1', + '//h3', + '//ul', + '//table[contains(@class, "in-article-item")]', + '//a[@target="_self"]', + '//div[@data-embed-type="Brightcove"]', + '//div[@class="QuoteContainer"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/allafrica.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/allafrica.com.php new file mode 100644 index 0000000..50a9bd6 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/allafrica.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.aljazeera.com/news/2015/09/xi-jinping-seattle-china-150922230118373.html', + 'body' => array( + '//div[@class="story-body"]', + ), + 'strip' => array( + '//p[@class="kindofstory"]', + '//cite[@class="byline"]', + '//div[contains(@class,"related-topics")]', + '//links', + '//sharebar', + '//related-topics', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/allgemeine-zeitung.de.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/allgemeine-zeitung.de.php new file mode 100644 index 0000000..4acdeaf --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/allgemeine-zeitung.de.php @@ -0,0 +1,24 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.allgemeine-zeitung.de/lokales/polizei/mainz-gonsenheim-unbekannte-rauben-esso-tankstelle-in-kurt-schumacher-strasse-aus_14913147.htm', + 'body' => array( + '//div[contains(@class, "article")][1]', + ), + 'strip' => array( + '//read/h1', + '//*[@id="t-map"]', + '//*[contains(@class, "modules")]', + '//*[contains(@class, "adsense")]', + '//*[contains(@class, "linkbox")]', + '//*[contains(@class, "info")]', + '//*[@class="skip"]', + '//*[@class="funcs"]', + '//span[@class="nd address"]', + '//a[contains(@href, "abo-und-services")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/amazingsuperpowers.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/amazingsuperpowers.com.php new file mode 100644 index 0000000..634fe37 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/amazingsuperpowers.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/anythingcomic.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/anythingcomic.com.php new file mode 100644 index 0000000..c73ac3b --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/anythingcomic.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'body' => array( + '//img[@id="comic_image"]', + '//div[@class="comment-wrapper"][position()=1]', + ), + 'strip' => array(), + 'test_url' => 'http://www.anythingcomic.com/comics/2108929/stress-free/', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/ap.org.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/ap.org.php new file mode 100644 index 0000000..6fadc7c --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/ap.org.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://hosted.ap.org/dynamic/stories/A/AS_CHINA_GAO_ZHISHENG?SITE=AP&SECTION=HOME&TEMPLATE=DEFAULT', + 'body' => array( + '//img[@class="ap-smallphoto-img"]', + '//span[@class="entry-content"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/areadvd.de.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/areadvd.de.php new file mode 100644 index 0000000..139fee5 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/areadvd.de.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.areadvd.de/news/daily-deals-angebote-bei-lautsprecher-teufel-3/', + 'body' => array('//div[contains(@class,"entry")]'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/arstechnica.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/arstechnica.com.php new file mode 100644 index 0000000..d4919a2 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/arstechnica.com.php @@ -0,0 +1,23 @@ + array( + '%.*%' => array( + 'test_url' => 'http://arstechnica.com/tech-policy/2015/09/judge-warners-2m-happy-birthday-copyright-is-bogus/', + 'body' => array( + '//header/h2', + '//section[@id="article-guts"]', + '//div[@class="superscroll-content show"]', + '//div[@class="gallery"]', + ), + 'next_page' => '//span[@class="numbers"]/a', + 'strip' => array( + '//figcaption', + '//div[@class="post-meta"]', + '//div[@class="gallery-image-credit"]', + '//aside', + '//div[@class="article-expander"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/awkwardzombie.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/awkwardzombie.com.php new file mode 100644 index 0000000..6dcb0e8 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/awkwardzombie.com.php @@ -0,0 +1,11 @@ + array( + '%/index.php.*comic=.*%' => array( + 'test_url' => 'http://www.awkwardzombie.com/index.php?comic=041315', + 'body' => array('//*[@id="comic"]/img'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/bangkokpost.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/bangkokpost.com.php new file mode 100644 index 0000000..4dab77c --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/bangkokpost.com.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bangkokpost.com/news/politics/704204/new-us-ambassador-arrives-in-bangkok', + 'body' => array( + '//div[@class="articleContents"]', + ), + 'strip' => array( + '//h2', + '//h4', + '//div[@class="text-size"]', + '//div[@class="relate-story"]', + '//div[@class="text-ads"]', + '//script', + '//ul', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/bgr.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/bgr.com.php new file mode 100644 index 0000000..d78709b --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/bgr.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://bgr.com/2015/09/27/iphone-6s-waterproof-testing/', + 'body' => array( + '//img[contains(@class,"img")]', + '//div[@class="text-column"]', + ), + 'strip' => array( + '//strong', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/bigfootjustice.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/bigfootjustice.com.php new file mode 100644 index 0000000..6d1c667 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/bigfootjustice.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/bizjournals.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/bizjournals.com.php new file mode 100644 index 0000000..6d8a5a9 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/bizjournals.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bizjournals.com/milwaukee/news/2015/09/30/bucks-will-hike-prices-on-best-seats-at-new-arena.html', + 'body' => array( + '//figure/div/a/img', + '//p[@class="content__segment"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/blog.fefe.de.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/blog.fefe.de.php new file mode 100644 index 0000000..d3ed697 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/blog.fefe.de.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://blog.fefe.de/?ts=ad706a73', + 'body' => array( + '/html/body/ul', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/blog.mapillary.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/blog.mapillary.com.php new file mode 100644 index 0000000..e67afe7 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/blog.mapillary.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://blog.mapillary.com/update/2015/08/26/traffic-sign-updates.html', + 'body' => array( + '//div[contains(@class, "blog-post__content")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/buenosairesherald.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/buenosairesherald.com.php new file mode 100644 index 0000000..7a6b33f --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/buenosairesherald.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.buenosairesherald.com/article/199344/manzur-named-next-governor-of-tucum%C3%A1n', + 'body' => array( + '//div[@style="float:none"]', + ), + 'strip' => array( + '//div[contains(@class, "bz_alias_short_desc_container"]', + '//td[@id="bz_show_bug_column_1"]', + '//table[@id="attachment_table"]', + '//table[@class="bz_comment_table"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/bunicomic.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/bunicomic.com.php new file mode 100644 index 0000000..fc54a81 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/bunicomic.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bunicomic.com/comic/buni-623/', + 'body' => array( + '//div[@class="comic-table"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/buttersafe.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/buttersafe.com.php new file mode 100644 index 0000000..04c860e --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/buttersafe.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://buttersafe.com/2015/04/21/the-incredible-flexible-man/', + 'body' => array( + '//div[@id="comic"]', + '//div[@class="post-comic"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/cad-comic.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/cad-comic.com.php new file mode 100644 index 0000000..5c05f51 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/cad-comic.com.php @@ -0,0 +1,13 @@ + array( + '%/cad/.+%' => array( + 'test_url' => 'http://www.cad-comic.com/cad/20150417', + 'body' => array( + '//*[@id="content"]/img', + ), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/chaoslife.findchaos.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/chaoslife.findchaos.com.php new file mode 100644 index 0000000..3bc711a --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/chaoslife.findchaos.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://chaoslife.findchaos.com/pets-in-the-wild', + 'body' => array('//div[@id="comic"]'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/cliquerefresh.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/cliquerefresh.com.php new file mode 100644 index 0000000..67a59c5 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/cliquerefresh.com.php @@ -0,0 +1,11 @@ + array( + '%/comic.*%' => array( + 'test_url' => 'http://cliquerefresh.com/comic/078-stating-the-obvious/', + 'body' => array('//div[@class="comicImg"]/img | //div[@class="comicImg"]/a/img'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/cnet.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/cnet.com.php new file mode 100644 index 0000000..4021968 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/cnet.com.php @@ -0,0 +1,38 @@ + array( + '%^/products.*%' => array( + 'test_url' => 'http://www.cnet.com/products/fibaro-flood-sensor/#ftag=CADf328eec', + 'body' => array( + '//li[contains(@class,"slide first"] || //figure[contains(@class,(promoFigure))]', + '//div[@class="quickInfo"]', + '//div[@class="col-6 ratings"]', + '//div[@id="editorReview"]', + ), + 'strip' => array( + '//script', + '//a[@class="clickToEnlarge"]', + '//div[@section="topSharebar"]', + '//div[contains(@class,"related")]', + '//div[contains(@class,"ad-")]', + '//div[@section="shortcodeGallery"]', + ), + ), + '%.*%' => array( + 'test_url' => 'http://cnet.com.feedsportal.com/c/34938/f/645093/s/4a340866/sc/28/l/0L0Scnet0N0Cnews0Cman0Eclaims0Eonline0Epsychic0Emade0Ehim0Ebuy0E10Emillion0Epowerball0Ewinning0Eticket0C0Tftag0FCAD590Aa51e/story01.htm', + 'body' => array( + '//p[@itemprop="description"]', + '//div[@itemprop="articleBody"]', + ), + 'strip' => array( + '//script', + '//a[@class="clickToEnlarge"]', + '//div[@section="topSharebar"]', + '//div[contains(@class,"related")]', + '//div[contains(@class,"ad-")]', + '//div[@section="shortcodeGallery"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/consomac.fr.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/consomac.fr.php new file mode 100644 index 0000000..31cf6cb --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/consomac.fr.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://consomac.fr/news-2430-l-iphone-6-toujours-un-secret-bien-garde.html', + 'body' => array( + '//div[contains(@id, "newscontent")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/cowbirdsinlove.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/cowbirdsinlove.com.php new file mode 100644 index 0000000..634fe37 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/cowbirdsinlove.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/csmonitor.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/csmonitor.com.php new file mode 100644 index 0000000..74b149d --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/csmonitor.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.csmonitor.com/USA/Politics/2015/0925/John-Boehner-steps-down-Self-sacrificing-but-will-it-lead-to-better-government', + 'body' => array( + '//figure[@id="image-top-1"]', + '//div[@id="story-body"]', + ), + 'strip' => array( + '//script', + '//img[@title="hide caption"]', + '//*[contains(@class,"promo_link")]', + '//div[@id="story-embed-column"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/dailyjs.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/dailyjs.com.php new file mode 100644 index 0000000..809a18a --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/dailyjs.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://dailyjs.com/2014/08/07/p5js/', + 'body' => array( + '//div[@id="post"]', + ), + 'strip' => array( + '//h2[@class="post"]', + '//div[@class="meta"]', + '//*[contains(@class, "addthis_toolbox")]', + '//*[contains(@class, "addthis_default_style")]', + '//*[@class="navigation small"]', + '//*[@id="related"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/dailyreporter.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/dailyreporter.com.php new file mode 100644 index 0000000..3ead1e7 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/dailyreporter.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://dailyreporter.com/2016/01/09/us-supreme-court-case-could-weaken-government-workers-unions/', + 'body' => array( + '//div[contains(@class, "entry-content")]', + ), + 'strip' => array( + '//div[@class="dmcss_login_form"]', + '//*[contains(@class, "sharedaddy")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/dailytech.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/dailytech.com.php new file mode 100644 index 0000000..19a23c9 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/dailytech.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.dailytech.com/Apples+First+Fixes+to+iOS+9+Land+w+iOS++901+Release/article37495.htm', + 'body' => array( + '//div[@class="NewsBodyImage"]', + '//span[@id="lblSummary"]', + '//span[@id="lblBody"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/degroupnews.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/degroupnews.com.php new file mode 100644 index 0000000..c166c51 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/degroupnews.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.degroupnews.com/medias/vodsvod/amazon-concurrence-la-chromecast-de-google-avec-fire-tv-stick', + 'body' => array( + '//div[@class="contenu"]', + ), + 'strip' => array( + '//div[contains(@class, "a2a")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/derstandard.at.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/derstandard.at.php new file mode 100644 index 0000000..66331e1 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/derstandard.at.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://derstandard.at/2000010267354/The-Witcher-3-Hohe-Hardware-Anforderungen-fuer-PC-Spieler?ref=rss', + 'body' => array( + '//div[@class="copytext"]', + '//ul[@id="media-list"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/dilbert.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/dilbert.com.php new file mode 100644 index 0000000..17f2ec2 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/dilbert.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'body' => array( + '//img[@class="img-responsive img-comic"]', + ), + 'test_url' => 'http://dilbert.com/strip/2016-01-28', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/discovermagazine.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/discovermagazine.com.php new file mode 100644 index 0000000..5b50ad1 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/discovermagazine.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://blogs.discovermagazine.com/the-extremo-files/2015/09/11/have-scientists-found-the-worlds-deepest-fish/', + 'body' => array( + '//div[@class="entry"]', + ), + 'strip' => array( + '//h1', + '//div[@class="meta"]', + '//div[@class="shareIcons"]', + '//div[@class="navigation"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/distrowatch.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/distrowatch.com.php new file mode 100644 index 0000000..ee0b977 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/distrowatch.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://distrowatch.com/?newsid=08355', + 'body' => array( + '//td[@class="NewsText"][1]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/dozodomo.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/dozodomo.com.php new file mode 100644 index 0000000..b894abf --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/dozodomo.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://dozodomo.com/bento/2014/03/04/lart-des-maki-de-takayo-kiyota/', + 'body' => array( + '//div[@class="joke"]', + '//div[@class="story-cover"]', + '//div[@class="story-content"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/drawingboardcomic.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/drawingboardcomic.com.php new file mode 100644 index 0000000..85c623a --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/drawingboardcomic.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'body' => array('//img[@id="comicimage"]'), + 'strip' => array(), + 'test_url' => 'http://drawingboardcomic.com/index.php?comic=208', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/encyclopedie.naheulbeuk.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/encyclopedie.naheulbeuk.com.php new file mode 100644 index 0000000..19bcbde --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/encyclopedie.naheulbeuk.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://encyclopedie.naheulbeuk.com/article.php3?id_article=352', + 'body' => array( + '//td//h1[@class="titre-texte"]', + '//td//div[@class="surtitre"]', + '//td//div[@class="texte"]', + ), + ) + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/endlessorigami.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/endlessorigami.com.php new file mode 100644 index 0000000..6d1c667 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/endlessorigami.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/engadget.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/engadget.com.php new file mode 100644 index 0000000..87775eb --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/engadget.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.engadget.com/2015/04/20/dark-matter-discovery/?ncid=rss_truncated', + 'body' => array('//div[@id="page_body"]/div[@class="container@m-"]'), + 'strip' => array('//aside[@role="banner"]'), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/escapistmagazine.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/escapistmagazine.com.php new file mode 100644 index 0000000..052fc0f --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/escapistmagazine.com.php @@ -0,0 +1,46 @@ + array( + '%/articles/view/comicsandcosplay/comics/critical-miss.*%' => array( + 'body' => array('//*[@class="body"]/span/img | //div[@class="folder_nav_links"]/following::p'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/critical-miss/13776-Critical-Miss-on-Framerates?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay/comics/namegame.*%' => array( + 'body' => array('//*[@class="body"]/span/p/img[@height != "120"]'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/namegame/9759-Leaving-the-Nest?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay/comics/stolen-pixels.*%' => array( + 'body' => array('//*[@class="body"]/span/p[2]/img'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/stolen-pixels/8866-Stolen-Pixels-258-Where-the-Boys-Are?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay/comics/bumhugparade.*%' => array( + 'body' => array('//*[@class="body"]/span/p[2]/img'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/bumhugparade/8262-Bumhug-Parade-13?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay.*/comics/escapistradiotheater%' => array( + 'body' => array('//*[@class="body"]/span/p[2]/img'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/escapistradiotheater/8265-The-Escapist-Radio-Theater-13?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay/comics/paused.*%' => array( + 'body' => array('//*[@class="body"]/span/p[2]/img | //*[@class="body"]/span/div/img'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/paused/8263-Paused-16?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay/comics/fraughtwithperil.*%' => array( + 'body' => array('//*[@class="body"]'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/fraughtwithperil/12166-The-Escapist-Presents-Escapist-Comics-Critical-Miss-B-lyeh-Fhlop?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/video-games/columns/.*%' => array( + 'body' => array('//*[@id="article_content"]'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/video-games/columns/experienced-points/13971-What-50-Shades-and-Batman-Have-in-Common.2', + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/espn.go.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/espn.go.com.php new file mode 100644 index 0000000..08cfd64 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/espn.go.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://espn.go.com/nfl/story/_/id/13388208/jason-whitlock-chip-kelly-controversy', + 'body' => array( + '//p', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/exocomics.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/exocomics.com.php new file mode 100644 index 0000000..1938281 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/exocomics.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'body' => array('//a[@class="comic"]/img'), + 'strip' => array(), + 'test_url' => 'http://www.exocomics.com/379', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/explosm.net.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/explosm.net.php new file mode 100644 index 0000000..1ea2efc --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/explosm.net.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://explosm.net/comics/3803/', + 'body' => array( + '//div[@id="comic-container"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/extrafabulouscomics.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/extrafabulouscomics.com.php new file mode 100644 index 0000000..29e0f19 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/extrafabulouscomics.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/fastcodesign.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/fastcodesign.com.php new file mode 100644 index 0000000..9cfa252 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/fastcodesign.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.fastcodesign.com/3026548/exposure/peek-inside-the-worlds-forbidden-subway-tunnels', + 'body' => array( + '//article[contains(@class, "body prose")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/fastcoexist.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/fastcoexist.com.php new file mode 100644 index 0000000..b1e42fe --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/fastcoexist.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.fastcoexist.com/3026114/take-a-seat-on-this-gates-funded-future-toilet-that-will-change-how-we-think-about-poop', + 'body' => array( + '//article[contains(@class, "body prose")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/fastcompany.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/fastcompany.com.php new file mode 100644 index 0000000..acade0c --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/fastcompany.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.fastcompany.com/3026712/fast-feed/elon-musk-an-apple-tesla-merger-is-very-unlikely', + 'body' => array( + '//article[contains(@class, "body prose")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/ffworld.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/ffworld.com.php new file mode 100644 index 0000000..7b99c2e --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/ffworld.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.ffworld.com/?rub=news&page=voir&id=2709', + 'body' => array( + '//div[@class="news_body"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/foreignpolicy.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/foreignpolicy.com.php new file mode 100644 index 0000000..6b0eb78 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/foreignpolicy.com.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://foreignpolicy.com/2016/01/09/networking-giant-pulls-nsa-linked-code-exploited-by-hackers/', + 'body' => array( + '//article', + ), + 'strip' => array( + '//div[@id="post-category"]', + '//div[@id="desktop-right"]', + '//h1', + '//section[@class="article-meta"]', + '//div[@class="side-panel-wrapper"]', + '//*[contains(@class, "share-")]', + '//*[contains(@id, "taboola-")]', + '//div[@class="comments"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/fossbytes.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/fossbytes.com.php new file mode 100644 index 0000000..cc011cd --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/fossbytes.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://fossbytes.com/fbi-hacked-1000-computers-to-shut-down-largest-child-pornography-site-on-the-dark-web/', + 'body' => array( + '//div[@class="entry-inner"]', + ), + 'strip' => array( + '//*[@class="at-above-post addthis_default_style addthis_toolbox at-wordpress-hide"]', + '//*[@class="at-below-post addthis_default_style addthis_toolbox at-wordpress-hide"]', + '//*[@class="at-below-post-recommended addthis_default_style addthis_toolbox at-wordpress-hide"]', + '//*[@class="code-block code-block-12 ai-desktop"]', + '//*[@class="code-block code-block-13 ai-tablet-phone"]', + ), + ), + ), +); \ No newline at end of file diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/fowllanguagecomics.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/fowllanguagecomics.com.php new file mode 100644 index 0000000..79ae240 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/fowllanguagecomics.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'body' => array('//*[@id="comic"] | //*[@class="post-image"]'), + 'strip' => array(), + 'test_url' => 'http://www.fowllanguagecomics.com/comic/working-out/', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/geek.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/geek.com.php new file mode 100644 index 0000000..1f00a88 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/geek.com.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.geek.com/news/the-11-best-ways-to-eat-eggs-1634076/', + 'body' => array( + '//div[@class="articleinfo"]/figure', + '//div[@class="articleinfo"]/article', + '//span[@class="by"]', + ), + 'strip' => array( + '//span[@class="red"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/gerbilwithajetpack.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/gerbilwithajetpack.com.php new file mode 100644 index 0000000..70e8635 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/gerbilwithajetpack.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'body' => array( + '//div[@id="comic-1"]', + '//div[@class="entry"]', + ), + 'test_url' => 'http://gerbilwithajetpack.com/passing-the-digital-buck/', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/giantitp.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/giantitp.com.php new file mode 100644 index 0000000..b7a6a9a --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/giantitp.com.php @@ -0,0 +1,13 @@ + array( + '%/comics/oots.*%' => array( + 'test_url' => 'http://www.giantitp.com/comics/oots0989.html', + 'body' => array( + '//td[@align="center"]/img', + ), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/github.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/github.com.php new file mode 100644 index 0000000..abc41ed --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/github.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'https://github.com/audreyr/favicon-cheat-sheet', + 'body' => array( + '//article[contains(@class, "entry-content")]', + ), + 'strip' => array( + '//h1', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/gocomics.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/gocomics.com.php new file mode 100644 index 0000000..5bfbe37 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/gocomics.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.gocomics.com/pearlsbeforeswine/2015/05/30', + 'body' => array( + '//div[1]/p[1]/a[1]/img', + ), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/golem.de.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/golem.de.php new file mode 100644 index 0000000..ea1af87 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/golem.de.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.golem.de/news/breko-telekom-verzoegert-gezielt-den-vectoring-ausbau-1311-102974.html', + 'body' => array( + '//header[@class="cluster-header"]', + '//div[@class="formatted"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/happletea.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/happletea.com.php new file mode 100644 index 0000000..5651bf4 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/happletea.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'body' => array( + '//div[@id="comic"]', + '//div[@class="entry"]', + ), + 'strip' => array('//div[@class="ssba"]'), + 'test_url' => 'http://www.happletea.com/comic/mans-best-friend/', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/heise.de.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/heise.de.php new file mode 100644 index 0000000..85904c0 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/heise.de.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.heise.de/security/meldung/BND-300-Millionen-Euro-fuer-Fruehwarnsystem-gegen-Cyber-Attacken-2192237.html', + 'body' => array( + '//div[@class="meldung_wrapper"]', + '//div[@class="artikel_content"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/huffingtonpost.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/huffingtonpost.com.php new file mode 100644 index 0000000..adbc581 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/huffingtonpost.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.huffingtonpost.com/2014/02/20/centscere-social-media-syracuse_n_4823848.html', + 'body' => array( + '//article[@class="content")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/imogenquest.net.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/imogenquest.net.php new file mode 100644 index 0000000..634fe37 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/imogenquest.net.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/ing.dk.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/ing.dk.php new file mode 100644 index 0000000..5081a26 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/ing.dk.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://ing.dk/artikel/smart-husisolering-og-styring-skal-mindske-japans-energikrise-164517', + 'body' => array( + '//section[contains(@class, "teaser")]', + '//section[contains(@class, "body")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/invisiblebread.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/invisiblebread.com.php new file mode 100644 index 0000000..e4fd6ef --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/invisiblebread.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%()%' => '$1', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/ir.amd.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/ir.amd.com.php new file mode 100644 index 0000000..112165a --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/ir.amd.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'body' => array('//span[@class="ccbnTxt"]'), + 'strip' => array(), + 'test_url' => 'http://ir.amd.com/phoenix.zhtml?c=74093&p=RssLanding&cat=news&id=2055819', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/japantimes.co.jp.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/japantimes.co.jp.php new file mode 100644 index 0000000..b0c8ff2 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/japantimes.co.jp.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.japantimes.co.jp/news/2015/09/27/world/social-issues-world/pope-meets-sex-abuse-victims-philadelphia-promises-accountability/', + 'body' => array( + '//article[@role="main"]', + ), + 'strip' => array( + '//script', + '//header', + '//div[contains(@class, "meta")]', + '//div[@class="clearfix"]', + '//div[@class="OUTBRAIN"]', + '//ul[@id="content_footer_menu"]', + '//div[@class="article_footer_ad"]', + '//div[@id="disqus_thread"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/japantoday.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/japantoday.com.php new file mode 100644 index 0000000..8334689 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/japantoday.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.japantoday.com/category/politics/view/japan-u-s-to-sign-new-base-environment-pact', + 'body' => array( + '//div[@id="article_container"]', + ), + 'strip' => array( + '//h2', + '//div[@id="article_info"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/journaldugeek.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/journaldugeek.com.php new file mode 100644 index 0000000..1034c9d --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/journaldugeek.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www./2014/05/20/le-playstation-now-arrive-en-beta-fermee-aux-etats-unis/', + 'body' => array( + '//div[@class="post-content"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/jsonline.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/jsonline.com.php new file mode 100644 index 0000000..9fd8d86 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/jsonline.com.php @@ -0,0 +1,24 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.jsonline.com/news/usandworld/as-many-as-a-million-expected-for-popes-last-mass-in-us-b99585180z1-329688131.html', + 'body' => array( + '//div[@id="main"]', + ), + 'strip' => array( + '//script', + 'div[contains(@class, "header")]', + 'div[@class="module--headline"]', + 'div[@class="main--inlinemeta"]', + 'div[contains(@class, "leftcol--")]', + 'p[@class="main--author"]', + 'div[@class="story--rightcol"]', + 'div[contains(@class, "footer")]', + 'div[contains(@class, "rightcol--")]', + 'div[contains(@class, "author")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/kanpai.fr.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/kanpai.fr.php new file mode 100644 index 0000000..a93ef6c --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/kanpai.fr.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.kanpai.fr/japon/comment-donner-lheure-en-japonais.html', + 'body' => array( + '//div[@class="single-left"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/karriere.jobfinder.dk.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/karriere.jobfinder.dk.php new file mode 100644 index 0000000..7c7b456 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/karriere.jobfinder.dk.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://karriere.jobfinder.dk/artikel/dansk-professor-skal-lede-smart-grid-forskning-20-millioner-dollars-763', + 'body' => array( + '//section[contains(@class, "teaser")]', + '//section[contains(@class, "body")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/koreaherald.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/koreaherald.com.php new file mode 100644 index 0000000..dacaa9b --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/koreaherald.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.koreaherald.com/view.php?ud=20150926000018', + 'body' => array( + '//div[@class="content_view"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/koreatimes.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/koreatimes.php new file mode 100644 index 0000000..95a9b56 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/koreatimes.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.koreatimes.co.kr/www/news/nation/2015/12/116_192409.html', + 'body' => array( + '//div[@id="p"]', + ), + 'strip' => array( + '//script', + '//div[@id="webtalks_btn_listenDiv"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lastplacecomics.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lastplacecomics.com.php new file mode 100644 index 0000000..29e0f19 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lastplacecomics.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lejapon.fr.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lejapon.fr.php new file mode 100644 index 0000000..07cfdbb --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lejapon.fr.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://lejapon.fr/guide-voyage-japon/5223/tokyo-sous-la-neige.htm', + 'body' => array( + '//div[@class="entry"]', + ), + 'strip' => array( + '//*[contains(@class, "addthis_toolbox")]', + '//*[contains(@class, "addthis_default_style")]', + '//*[@class="navigation small"]', + '//*[@id="related"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lesjoiesducode.fr.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lesjoiesducode.fr.php new file mode 100644 index 0000000..12177e9 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lesjoiesducode.fr.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://lesjoiesducode.fr/post/75576211207/quand-lappli-ne-fonctionne-plus-sans-aucune-raison', + 'body' => array( + '//div[@class="blog-post-content"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lfg.co.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lfg.co.php new file mode 100644 index 0000000..1073f0e --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lfg.co.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.lfg.co/page/871/?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+LookingForGroup+%28Looking+For+Group%29&utm_content=FeedBurner', + 'body' => array( + '//*[@id="comic"]/img | //*[@class="content"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lifehacker.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lifehacker.com.php new file mode 100644 index 0000000..a42df27 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lifehacker.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://lifehacker.com/bring-water-bottle-caps-into-concerts-to-protect-your-d-1269334973', + 'body' => array( + '//div[contains(@class, "row")/img', + '//div[contains(@class, "content-column")]', + ), + 'strip' => array( + '//*[contains(@class, "meta")]', + '//span[contains(@class, "icon")]', + '//h1', + '//aside', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/linux.org.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/linux.org.php new file mode 100644 index 0000000..c31bc8d --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/linux.org.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.linux.org/threads/lua-the-scripting-interpreter.8352/', + 'body' => array( + '//div[@class="messageContent"]', + ), + 'strip' => array( + '//aside', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/linuxinsider.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/linuxinsider.com.php new file mode 100644 index 0000000..2ad69cc --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/linuxinsider.com.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.linuxinsider.com/story/82526.html?rss=1', + 'body' => array( + '//div[@id="story"]', + ), + 'strip' => array( + '//script', + '//h1', + '//div[@id="story-toolbox1"]', + '//div[@id="story-byline"]', + '//div[@id="story"]/p', + '//div[@class="story-advertisement"]', + '//iframe', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lists.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lists.php new file mode 100644 index 0000000..c707551 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lists.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://lists.freebsd.org/pipermail/freebsd-announce/2013-September/001504.html', + 'body' => array( + '//pre', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/loadingartist.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/loadingartist.com.php new file mode 100644 index 0000000..6d1c667 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/loadingartist.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/loldwell.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/loldwell.com.php new file mode 100644 index 0000000..6954ebf --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/loldwell.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://loldwell.com/?comic=food-math-101', + 'body' => array('//*[@id="comic"]'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lukesurl.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lukesurl.com.php new file mode 100644 index 0000000..15850e4 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/lukesurl.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'body' => array('//div[@id="comic"]//img'), + 'strip' => array(), + 'test_url' => 'http://www.lukesurl.com/archives/comic/665-3-of-clubs', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/macg.co.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/macg.co.php new file mode 100644 index 0000000..1d84ffa --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/macg.co.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.macg.co//logiciels/2014/05/feedly-sameliore-un-petit-peu-sur-mac-82205', + 'body' => array( + '//div[contains(@class, "field-name-body")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/marc.info.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/marc.info.php new file mode 100644 index 0000000..5603940 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/marc.info.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://marc.info/?l=openbsd-misc&m=141987113202061&w=2', + 'body' => array( + '//pre', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/marriedtothesea.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/marriedtothesea.com.php new file mode 100644 index 0000000..b33b0d0 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/marriedtothesea.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.marriedtothesea.com/index.php?date=052915', + 'body' => array( + '//div[@align]/a/img', + ), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/marycagle.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/marycagle.com.php new file mode 100644 index 0000000..3a8c783 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/marycagle.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'body' => array( + '//img[@id="cc-comic"]', + '//div[@class="cc-newsbody"]', + ), + 'strip' => array(), + 'test_url' => 'http://www.marycagle.com/letsspeakenglish/74-grim-reality/', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/maximumble.thebookofbiff.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/maximumble.thebookofbiff.com.php new file mode 100644 index 0000000..a94a166 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/maximumble.thebookofbiff.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://maximumble.thebookofbiff.com/2015/04/20/1084-change/', + 'body' => array('//div[@id="comic"]/div/a/img'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/medium.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/medium.com.php new file mode 100644 index 0000000..880a42c --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/medium.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'https://medium.com/lessons-learned/917b8b63ae3e', + 'body' => array( + '//div[contains(@class, "post-field body")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/mercworks.net.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/mercworks.net.php new file mode 100644 index 0000000..e478ddf --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/mercworks.net.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'body' => array('//div[@id="comic"]', + '//div[contains(@class,"entry-content")]', + ), + 'strip' => array(), + 'test_url' => 'http://mercworks.net/comicland/healthy-choice/', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/metronieuws.nl.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/metronieuws.nl.php new file mode 100644 index 0000000..ee505a1 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/metronieuws.nl.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.metronieuws.nl/sport/2015/04/broer-fellaini-zorgde-bijna-voor-paniek-bij-mourinho', + 'body' => array('//div[contains(@class,"article-top")]/div[contains(@class,"image-component")] | //div[@class="article-full-width"]/div[1]'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/milwaukeenns.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/milwaukeenns.php new file mode 100644 index 0000000..58677a6 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/milwaukeenns.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://milwaukeenns.org/2016/01/08/united-way-grant-enables-sdc-to-restore-free-tax-assistance-program/', + 'body' => array( + '//div[@class="pf-content"]', + ), + 'strip' => array( + '//div[@class="printfriendly"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/mlb.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/mlb.com.php new file mode 100644 index 0000000..55dbfff --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/mlb.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://m.brewers.mlb.com/news/article/161364798', + 'body' => array( + '//article', + ), + 'strip' => array( + '//div[@class="article-top"]', + '//div[contains(@class, "contributor-bottom")]', + '//p[@class="tagline"]', + '//div[contains(@class, "social-")]', + '//div[@class="button-wrap"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/mokepon.smackjeeves.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/mokepon.smackjeeves.com.php new file mode 100644 index 0000000..b76812b --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/mokepon.smackjeeves.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://mokepon.smackjeeves.com/comics/2120096/chapter-9-page-68/', + 'body' => array('//*[@id="comic_area_inner"]/img | //*[@id="comic_area_inner"]/a/img'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/monwindowsphone.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/monwindowsphone.com.php new file mode 100644 index 0000000..47c93d7 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/monwindowsphone.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.monwindowsphone.com/tout-savoir-sur-le-centre-d-action-de-windows-phone-8-1-t40574.html', + 'body' => array( + '//div[@class="blog-post-body"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/mrlovenstein.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/mrlovenstein.com.php new file mode 100644 index 0000000..fb6ae75 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/mrlovenstein.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + '%alt="(.+)" */>%' => '/>
$1', + '%\.png%' => '_rollover.png', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/muckrock.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/muckrock.com.php new file mode 100644 index 0000000..845b18e --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/muckrock.com.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.muckrock.com/news/archives/2016/jan/13/5-concerns-private-prisons/', + 'body' => array( + '//div[@class="content"]', + ), + 'strip' => array( + '//h1', + '//div[@class="secondary"]', + '//aside', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/nationaljournal.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/nationaljournal.com.php new file mode 100644 index 0000000..124afc3 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/nationaljournal.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nationaljournal.com/s/354962/south-carolina-evangelicals-outstrip-establishment?mref=home_top_main', + 'body' => array( + '//div[@class="section-body"]', + ), + 'strip' => array( + '//*[contains(@class, "-related")]', + '//*[contains(@class, "social")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/nature.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/nature.com.php new file mode 100644 index 0000000..6b9e87f --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/nature.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nature.com/doifinder/10.1038/nature.2015.18340', + 'body' => array( + '//div[contains(@class,"main-content")]', + ), + 'strip' => array(), + ), + ), +); + diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/nba.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/nba.com.php new file mode 100644 index 0000000..a9afb62 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/nba.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nba.com/2015/news/09/25/knicks-jackson-to-spend-more-time-around-coaching-staff.ap/index.html?rss=true', + 'body' => array( + '//section[@id="nbaArticleContent"]', + ), + 'strip' => array( + '//div[@id="nbaArticleSocialWrapper_bot"]', + '//h5', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/nedroid.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/nedroid.com.php new file mode 100644 index 0000000..634fe37 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/nedroid.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/networkworld.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/networkworld.com.php new file mode 100644 index 0000000..e5485f6 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/networkworld.com.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.networkworld.com/article/3020585/security/the-incident-response-fab-five.html', + 'body' => array( + '//figure/img[@class="hero-img"]', + '//section[@class="deck"]', + '//div[@itemprop="articleBody"] | //div[@itemprop="reviewBody"]', + '//div[@class="carousel-inside-crop"]', + ), + 'strip' => array( + '//script', + '//aside', + '//div[@class="credit"]', + '//div[@class="view-large"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/neustadt-ticker.de.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/neustadt-ticker.de.php new file mode 100644 index 0000000..60d9bfa --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/neustadt-ticker.de.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.neustadt-ticker.de/41302/alltag/kultur/demo-auf-der-boehmischen', + 'body' => array( + '//div[@class="entry-content"]', + ), + 'strip' => array( + '//*[contains(@class, "sharedaddy")]', + '//*[contains(@class, "yarpp-related")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/niceteethcomic.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/niceteethcomic.com.php new file mode 100644 index 0000000..a1d495d --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/niceteethcomic.com.php @@ -0,0 +1,11 @@ + array( + '%/archives.*%' => array( + 'test_url' => 'http://niceteethcomic.com/archives/page119/', + 'body' => array('//*[@class="comicpane"]/a/img'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/nichtlustig.de.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/nichtlustig.de.php new file mode 100644 index 0000000..2083fdd --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/nichtlustig.de.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%.*static.nichtlustig.de/comics/full/(\\d+).*%s' => '', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/oglaf.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/oglaf.com.php new file mode 100644 index 0000000..8614985 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/oglaf.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'body' => array( + '//img[@id="strip"]', + '//a/div[@id="nx"]/..', + ), + 'strip' => array(), + 'test_url' => 'http://oglaf.com/slodging/', + ), + ), + 'filter' => array( + '%.*%' => array( + '%alt="(.+)" title="(.+)" */>%' => '/>
$1
$2
', + '%%' => 'Next page', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/onhax.net.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/onhax.net.php new file mode 100644 index 0000000..d2e2894 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/onhax.net.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://onhax.net/process-lasso-8-9-1-4-pro-key-portable-is-here-latest', + 'body' => array( + '//div[@class="postcontent"]', + ), + 'strip' => array( + '//*[@class="sharedaddy sd-sharing-enabled"]', + '//*[@class="yarpp-related"]', + ), + ), + ), +); \ No newline at end of file diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/onmilwaukee.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/onmilwaukee.php new file mode 100644 index 0000000..2f74aac --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/onmilwaukee.php @@ -0,0 +1,25 @@ + array( + '%.*%' => array( + 'test_url' => 'http://onmilwaukee.com/movies/articles/downerspelunking.html', + 'body' => array( + '//article[contains(@class, "show")]', + ), + 'strip' => array( + '//h1', + '//div[contains(@class, "-ad")]', + '//div[contains(@class, "_ad")]', + '//div[@id="pub_wrapper"]', + '//div[contains(@class, "share_tools")]', + '//div[@class="clearfix"]', + '//div[contains(@class,"image_control")]', + '//section[@class="ribboned"]', + '//div[contains(@class,"sidebar")]', + '//aside[@class="article_tag_list"]', + '//section[contains(@id, "more_posts")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/openrightsgroup.org.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/openrightsgroup.org.php new file mode 100644 index 0000000..0440009 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/openrightsgroup.org.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.openrightsgroup.org/blog/2014/3-days-to-go-till-orgcon2014', + 'body' => array( + '//div[contains(@class, "content")]/div', + ), + 'strip' => array( + '//h2[1]', + '//div[@class="info"]', + '//div[@class="tags"]', + '//div[@class="comments"]', + '//div[@class="breadcrumbs"]', + '//h1[@class="pageTitle"]', + '//p[@class="bookmarkThis"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/opensource.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/opensource.com.php new file mode 100644 index 0000000..52c2ffa --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/opensource.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://opensource.com/life/15/10/how-internet-things-will-change-way-we-think', + 'body' => array( + '//img[@class="image-full-size"]', + '//div[contains(@class="field-type-text-with-summary")]', + ), + ) + ) +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/optipess.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/optipess.com.php new file mode 100644 index 0000000..634fe37 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/optipess.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/osnews.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/osnews.com.php new file mode 100644 index 0000000..10bd0b1 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/osnews.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://osnews.com/story/28863/Google_said_to_be_under_US_antitrust_scrutiny_over_Android', + 'body' => array( + '//div[@class="newscontent1"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/pastebin.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/pastebin.com.php new file mode 100644 index 0000000..4cfc859 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/pastebin.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://pastebin.com/ed1pP9Ak', + 'body' => array( + '//div[@class="text"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/peebleslab.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/peebleslab.com.php new file mode 100644 index 0000000..0689f87 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/peebleslab.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + // the extra space is required to strip the title cleanly + '%title="(.+) " */>%' => '/>
$1', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/penny-arcade.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/penny-arcade.com.php new file mode 100644 index 0000000..84596fa --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/penny-arcade.com.php @@ -0,0 +1,22 @@ + array( + '%/news/.*%' => array( + 'test_url' => 'http://penny-arcade.com/news/post/2015/04/15/101-part-two', + 'body' => array( + '//*[@class="postBody"]/*', + ), + 'strip' => array( + ), + ), + '%/comic/.*%' => array( + 'test_url' => 'http://penny-arcade.com/comic/2015/04/15', + 'body' => array( + '//*[@id="comicFrame"]/a/img', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/pixelbeat.org.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/pixelbeat.org.php new file mode 100644 index 0000000..289f839 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/pixelbeat.org.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.pixelbeat.org/programming/sigpipe_handling.html#1425573246', + 'body' => array( + '//div[@class="contentText"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/plus.google.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/plus.google.com.php new file mode 100644 index 0000000..f6e3d37 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/plus.google.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'https://plus.google.com/+LarryPage/posts/Lh8SKC6sED1', + 'body' => array( + '//div[@role="article"]/div[contains(@class, "eE")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/popstrip.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/popstrip.com.php new file mode 100644 index 0000000..7c4da00 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/popstrip.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%( '$1$2$1bonus.png"/>', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/putaindecode.fr.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/putaindecode.fr.php new file mode 100644 index 0000000..f3885f5 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/putaindecode.fr.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://putaindecode.fr/posts/js/etat-lieux-js-modulaire-front/', + 'body' => array( + '//*[@class="putainde-Post-md"]', + ), + 'strip' => array( + '//*[contains(@class, "inlineimg")]', + '//*[contains(@class, "comment-respond")]', + '//header', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/recode.net.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/recode.net.php new file mode 100644 index 0000000..5511411 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/recode.net.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://recode.net/2015/09/26/big-tech-rolls-out-red-carpet-for-indian-prime-minister-lobbies-behind-closed-doors/', + 'body' => array( + '//img[contains(@class,"attachment-large")]', + '//div[contains(@class,"postarea")]', + '//li[@class,"author"]', + ), + 'strip' => array( + '//script', + '//div[contains(@class,"sharedaddy")]', + '//div[@class="post-send-off"]', + '//div[@class="large-12 columns"]', + '//div[contains(@class,"inner-related-article")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/retractionwatch.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/retractionwatch.com.php new file mode 100644 index 0000000..b97c73e --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/retractionwatch.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://retractionwatch.com/2015/11/12/psychologist-jens-forster-settles-case-by-agreeing-to-2-retractions/', + 'body' => array( + '//*[@class="main"]', + '//*[@class="entry-content"]', + ), + 'strip' => array( + '//*[contains(@class, "sharedaddy")]', + '//*[contains(@class, "jp-relatedposts")]', + '//p[@class="p1"]', + ) + ) + ) +); + diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/rue89.nouvelobs.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/rue89.nouvelobs.com.php new file mode 100644 index 0000000..ba5bb48 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/rue89.nouvelobs.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://rue89.feedsportal.com/c/33822/f/608948/s/30999fa0/sc/24/l/0L0Srue890N0C20A130C0A80C30A0Cfaisait0Eboris0Eboillon0Eex0Esarko0Eboy0E350A0E0A0A0A0Eeuros0Egare0Enord0E245315/story01.htm', + 'body' => array( + '//*[@id="article"]/div[contains(@class, "content")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/rugbyrama.fr.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/rugbyrama.fr.php new file mode 100644 index 0000000..2280b66 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/rugbyrama.fr.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.rugbyrama.fr/rugby/top-14/2015-2016/top-14-hayman-coupe-du-monde-finale-2012-lutte.-voici-levan-chilachava-toulon_sto5283863/story.shtml', + 'body' => array( + '//div[@class="story-simple-content"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//*[@class="share-buttons"]', + '//*[@class="show-mobile-block"]', + '//*[@class="hide-desktop"]', + '//*[@id="tracking_img"]', + ) + ) + ) +); \ No newline at end of file diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/satwcomic.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/satwcomic.com.php new file mode 100644 index 0000000..836cc97 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/satwcomic.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://satwcomic.com/day-at-the-beach', + 'body' => array( + '//div[@class="container"]/center/a/img', + '//span[@itemprop="articleBody"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/scrumalliance.org.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/scrumalliance.org.php new file mode 100644 index 0000000..1715edd --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/scrumalliance.org.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.scrumalliance.org/community/articles/2015/march/an-introduction-to-agile-project-intake?feed=articles', + 'body' => array( + '//div[@class="article_content"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/securityfocus.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/securityfocus.com.php new file mode 100644 index 0000000..0b1b98b --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/securityfocus.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.securityfocus.com/news/11569?ref=rss', + 'body' => array( + '//div[@class="expanded"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/sentfromthemoon.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/sentfromthemoon.com.php new file mode 100644 index 0000000..94955b2 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/sentfromthemoon.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'body' => array( + '//div[@class="comicpane"]/a/img', + '//div[@class="entry"]', + ), + 'strip' => array(), + 'test_url' => 'http://sentfromthemoon.com/archives/1417', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/sitepoint.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/sitepoint.com.php new file mode 100644 index 0000000..b7b9fbe --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/sitepoint.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.sitepoint.com/creating-hello-world-app-swift/', + 'body' => array( + '//section[@class="article_body"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/slashdot.org.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/slashdot.org.php new file mode 100644 index 0000000..f3496ba --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/slashdot.org.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://science.slashdot.org/story/15/04/20/0528253/pull-top-can-tabs-at-50-reach-historic-archaeological-status', + 'body' => array( + '//article/div[@class="body"] | //article[@class="layout-article"]/div[@class="elips"]', ), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/smallhousebliss.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/smallhousebliss.com.php new file mode 100644 index 0000000..bc0aa48 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/smallhousebliss.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://smallhousebliss.com/2013/08/29/house-g-by-lode-architecture/', + 'body' => array( + '//div[@class="post-content"]', + ), + 'strip' => array( + '//*[contains(@class, "gallery")]', + '//*[contains(@class, "share")]', + '//*[contains(@class, "wpcnt")]', + '//*[contains(@class, "meta")]', + '//*[contains(@class, "postitle")]', + '//*[@id="nav-below"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/smarthomewelt.de.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/smarthomewelt.de.php new file mode 100644 index 0000000..fe6e79a --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/smarthomewelt.de.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://smarthomewelt.de/apple-tv-amazon-echo-smart-home/', + 'body' => array('//div[@class="entry-inner"]/p | //div[@class="entry-inner"]/div[contains(@class,"wp-caption")]'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/smashingmagazine.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/smashingmagazine.com.php new file mode 100644 index 0000000..2e95d1c --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/smashingmagazine.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.smashingmagazine.com/2015/04/17/using-sketch-for-responsive-web-design-case-study/', + 'body' => array('//article[contains(@class,"post")]/p'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/smbc-comics.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/smbc-comics.com.php new file mode 100644 index 0000000..cbf6c99 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/smbc-comics.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%()%' => '$1$2$1after$2', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/soundandvision.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/soundandvision.com.php new file mode 100644 index 0000000..214ba29 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/soundandvision.com.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.soundandvision.com/content/james-guthrie-mixing-roger-waters-and-pink-floyd-51', + 'body' => array( + '//div[@id="left"]', + ), + 'strip' => array( + '//div[@class="meta"]', + '//div[@class="ratingsbox"]', + '//h1', + '//h2', + '//addthis', + '//comment-links', + '//div[@class="book-navigation"]', + '//div[@class="comment-links"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/spiegel.de.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/spiegel.de.php new file mode 100644 index 0000000..9348ae0 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/spiegel.de.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.spiegel.de/politik/ausland/afrika-angola-geht-gegen-islam-vor-und-schliesst-moscheen-a-935788.html', + 'body' => array( + '//div[contains(@class, "article-section")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/stereophile.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/stereophile.com.php new file mode 100644 index 0000000..9ea43b3 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/stereophile.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.stereophile.com/content/2015-rocky-mountain-audio-fest-starts-friday', + 'body' => array( + '//div[@class="content clear-block"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/stupidfox.net.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/stupidfox.net.php new file mode 100644 index 0000000..b8aef24 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/stupidfox.net.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://stupidfox.net/134-sleepy-time', + 'body' => array( + '//div[@class="comicmid"]/center/a/img', + '//div[@class="stand_high"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/subtraction.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/subtraction.com.php new file mode 100644 index 0000000..37ce1cc --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/subtraction.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.subtraction.com/2015/06/06/time-lapse-video-of-one-world-trade-center/', + 'body' => array('//article/div[@class="entry-content"]'), + 'strip' => array(), + ), + ), + 'filter' => array( + '%.*%' => array( + '%\+%' => '', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/sz.de.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/sz.de.php new file mode 100644 index 0000000..53f8b6c --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/sz.de.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://sz.de/1.2443161', + 'body' => array('//article[@id="sitecontent"]/section[@class="topenrichment"]//img | //article[@id="sitecontent"]/section[@class="body"]/section[@class="authors"]/preceding-sibling::*[not(contains(@class, "ad"))]'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/techcrunch.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/techcrunch.com.php new file mode 100644 index 0000000..8671ac5 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/techcrunch.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://techcrunch.com/2013/08/31/indias-visa-maze/', + 'body' => array( + '//div[contains(@class, "media-container")]', + '//div[@class="body-copy"]', + ), + 'strip' => array( + '//*[contains(@class, "module-crunchbase")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/the-ebook-reader.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/the-ebook-reader.com.php new file mode 100644 index 0000000..127b216 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/the-ebook-reader.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://blog.the-ebook-reader.com/2015/09/25/kobo-glo-hd-and-kobo-touch-2-0-covers-and-cases-roundup/', + 'body' => array( + '//div[@class="entry"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/theatlantic.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/theatlantic.com.php new file mode 100644 index 0000000..0c3201b --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/theatlantic.com.php @@ -0,0 +1,23 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.theatlantic.com/politics/archive/2015/09/what-does-it-mean-to-lament-the-poor-inside-panem/407317/', + 'body' => array( + '//picture[@class="img"]', + '//figure/figcaption/span', + '//div/p[@itemprop="description"]', + '//div[@class="article-body"]', + ), + 'strip' => array( + '//aside[@class="callout"]', + '//span[@class="credit"]', + '//figcaption[@class="credit"]', + '//aside[contains(@class,"partner-box")]', + '//div[contains(@class,"ad")]', + '//a[contains(@class,"social-icon")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/theawkwardyeti.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/theawkwardyeti.com.php new file mode 100644 index 0000000..29e0f19 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/theawkwardyeti.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thecodinglove.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thecodinglove.com.php new file mode 100644 index 0000000..fb7f174 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thecodinglove.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://thecodinglove.com/post/116897934767', + 'body' => array('//div[@class="bodytype"]'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thedoghousediaries.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thedoghousediaries.com.php new file mode 100644 index 0000000..e1c4970 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thedoghousediaries.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'body' => array( + '//div[@class="comicpane"]/a/img', + '//div[@class="entry"]', + ), + 'strip' => array(), + 'test_url' => 'http://thedoghousediaries.com/6023', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thegamercat.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thegamercat.com.php new file mode 100644 index 0000000..e1a1318 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thegamercat.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.thegamercat.com/comic/just-no/', + 'body' => array('//div[@id="comic"] | //div[@class="post-content"]/div[@class="entry"]/p'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thehindu.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thehindu.com.php new file mode 100644 index 0000000..f743985 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thehindu.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.thehindu.com/sci-tech/science/why-is-the-shape-of-cells-in-a-honeycomb-always-hexagonal/article7692306.ece?utm_source=RSS_Feed&utm_medium=RSS&utm_campaign=RSS_Syndication', + 'body' => array( + '//div/img[@class="main-image"]', + '//div[@class="photo-caption"]', + '//div[@class="articleLead"]', + '//p', + '//span[@class="upper"]', + ), + 'strip' => array( + '//div[@id="articleKeywords"]', + '//div[@class="photo-source"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thelocal.se.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thelocal.se.php new file mode 100644 index 0000000..4e6051e --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thelocal.se.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.thelocal.se/20151018/swedish-moderates-tighten-focus-on-begging-ban', + 'body' => array( + '//article', + ), + 'strip' => array( + '//p[@id="mobile-signature"]', + '//article/div[4]', + '//article/ul[1]', + '//div[@class="clr"]', + '//p[@class="small"]', + '//p[@style="font-weight: bold; font-size: 14px;"]', + '//div[@class="author"]', + '//div[@class="ad_container"]', + ) + ) + ) +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/themerepublic.net.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/themerepublic.net.php new file mode 100644 index 0000000..d537eb8 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/themerepublic.net.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.themerepublic.net/2015/04/david-lopez-pitoko.html?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+blogspot%2FDngUJ+%28Theme+Republic%29&utm_content=FeedBurner', + 'body' => array('//*[@class="post-body"]'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/themoscowtimes.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/themoscowtimes.com.php new file mode 100644 index 0000000..db21b15 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/themoscowtimes.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.themoscowtimes.com/business/article/535500.html', + 'body' => array( + '//div[@class="article_main_img"]', + '//div[@class="article_text"]', + ), + 'strip' => array( + '//div[@class="articlebottom"]', + '//p/b', + '//p/a[contains(@href, "/article.php?id=")]', + '//div[@class="disqus_wrap"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thenewslens.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thenewslens.com.php new file mode 100644 index 0000000..8085cff --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thenewslens.com.php @@ -0,0 +1,28 @@ + array( + '%.*%' => array( + 'test_url' => 'http://international.thenewslens.com/post/255032/', + 'body' => array( + '//article/main[contains(@class, "content-post")]', + ), + 'strip' => array( + '//div[@class="photo-credit"]', + '//p[@align="center"]', + '//div[@class="clearfix"]', + '//div[@class="authorZone"]', + '//style', + '//div[@id="ttsbox"]', + '//div[@id="ttscontrols"]', + '//div[@class="author-info"]', + '//div[contains(@id, "-ad")]', + '//div[@style="font-size:small;margin:3px 0 0 0;vertical-align:top;line-height:24px;"]', + '//div[contains(@class, "hidden-xs")]', + '//div[contains(@class, "visible-xs")]', + '//div[contains(@class, "visible-lg")]', + '//a[@name="comment-panel"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/theodd1sout.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/theodd1sout.com.php new file mode 100644 index 0000000..6d1c667 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/theodd1sout.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/theonion.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/theonion.com.php new file mode 100644 index 0000000..c1a51da --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/theonion.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.theonion.com/article/wild-eyed-jim-harbaugh-informs-players-they-must-k-51397?utm_medium=RSS&utm_campaign=feeds', + 'body' => array( + '//div[@class="content-masthead"]/figure/div/noscript/img', + '//div[@class="content-text"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thestandard.com.hk.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thestandard.com.hk.php new file mode 100644 index 0000000..31ea096 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/thestandard.com.hk.php @@ -0,0 +1,23 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.thestandard.com.hk/breaking_news_detail.asp?id=67156', + 'body' => array( + '//table/tr/td/span[@class="bodyCopy"]', + ), + 'strip' => array( + '//script', + '//br', + '//map[@name="gif_bar"]', + '//img[contains(@usemap,"gif_bar")]', + '//a', + '//span[@class="bodyHeadline"]', + '//i', + '//b', + '//table', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/threepanelsoul.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/threepanelsoul.com.php new file mode 100644 index 0000000..5407158 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/threepanelsoul.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'body' => array( + '//img[@id="cc-comic"]', + ), + 'test_url' => 'http://www.threepanelsoul.com/comic/uncloaking', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/timesofindia.indiatimes.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/timesofindia.indiatimes.com.php new file mode 100644 index 0000000..873a46d --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/timesofindia.indiatimes.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://timesofindia.indiatimes.com/city/mangaluru/Adani-UPCL-to-release-CSR-grant-of-Rs-3-74-crore-to-YellurGram-Panchayat/articleshow/50512116.cms', + 'body' => array( + '//div[@class="article_content clearfix"]', + '//section[@class="highlight clearfix"]', + ), + 'strip' => array( + ), + ), + ), +); \ No newline at end of file diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/travel-dealz.de.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/travel-dealz.de.php new file mode 100644 index 0000000..76d22ca --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/travel-dealz.de.php @@ -0,0 +1,16 @@ + array( + '%^/blog.*%' => array( + 'test_url' => 'http://travel-dealz.de/blog/venere-gutschein/', + 'body' => array('//div[@class="post-entry"]'), + 'strip' => array( + '//*[@id="jp-relatedposts"]', + '//*[@class="post-meta"]', + '//*[@class="post-data"]', + '//*[@id="author-meta"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/treehugger.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/treehugger.com.php new file mode 100644 index 0000000..8be61aa --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/treehugger.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.treehugger.com/uncategorized/top-ten-posts-week-bunnies-2.html', + 'body' => array( + '//div[contains(@class, "promo-image")]', + '//div[contains(@id, "entry-body")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/treelobsters.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/treelobsters.com.php new file mode 100644 index 0000000..634fe37 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/treelobsters.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/twogag.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/twogag.com.php new file mode 100644 index 0000000..36c260d --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/twogag.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%http://www.twogag.com/comics-rss/([^.]+)\\.jpg%' => 'http://www.twogag.com/comics/$1.jpg', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/twokinds.keenspot.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/twokinds.keenspot.com.php new file mode 100644 index 0000000..f86ae34 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/twokinds.keenspot.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://twokinds.keenspot.com/archive.php?p=0', + 'body' => array('//*[@class="comic"]/div/a/img | //*[@class="comic"]/div/img | //*[@id="cg_img"]/img | //*[@id="cg_img"]/a/img'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/undeadly.org.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/undeadly.org.php new file mode 100644 index 0000000..3fe8261 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/undeadly.org.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://undeadly.org/cgi?action=article&sid=20141101181155', + 'body' => array( + '/html/body/table[3]/tbody/tr/td[1]/table[2]/tr/td[1]', + ), + 'strip' => array( + '//font', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/upi.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/upi.com.php new file mode 100644 index 0000000..cc34eb1 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/upi.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.upi.com/Top_News/US/2015/09/26/Tech-giants-Hollywood-stars-among-guests-at-state-dinner-for-Chinas-Xi-Jinping/4541443281006/', + 'body' => array( + '//div[@class="img"]', + '//div/article[@itemprop="articleBody"]', + ), + 'strip' => array( + '//div[@align="center"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/version2.dk.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/version2.dk.php new file mode 100644 index 0000000..a961b90 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/version2.dk.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.version2.dk/artikel/surface-pro-2-fungerer-bedre-til-arbejde-end-fornoejelse-55195', + 'body' => array( + '//section[contains(@class, "teaser")]', + '//section[contains(@class, "body")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/vgcats.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/vgcats.com.php new file mode 100644 index 0000000..c36b16c --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/vgcats.com.php @@ -0,0 +1,16 @@ + array( + '%/comics.*%' => array( + 'test_url' => 'http://www.vgcats.com/comics/?strip_id=358', + 'body' => array('//*[@align="center"]/img'), + 'strip' => array(), + ), + '%/super.*%' => array( + 'test_url' => 'http://www.vgcats.com/super/?strip_id=84', + 'body' => array('//*[@align="center"]/p/img'), + 'strip' => array(), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/vuxml.org.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/vuxml.org.php new file mode 100644 index 0000000..d8c286e --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/vuxml.org.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.vuxml.org/freebsd/a5f160fa-deee-11e4-99f8-080027ef73ec.html', + 'body' => array( + '//body', + ), + 'strip' => array( + '//h1', + '//div[@class="blurb"]', + '//hr', + '//p[@class="copyright"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.bbc.co.uk.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.bbc.co.uk.php new file mode 100644 index 0000000..1996c3d --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.bbc.co.uk.php @@ -0,0 +1,34 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bbc.co.uk/news/world-middle-east-23911833', + 'body' => array( + '//div[@class="story-body__inner"] | //div[@class="article"]', + '//div[@class="indPost"]', + ), + 'strip' => array( + '//form', + '//div[@id="headline"]', + '//*[@class="warning"]', + '//span[@class="off-screen"]', + '//span[@class="story-image-copyright"]', + '//ul[@class="story-body__unordered-list"]', + '//div[@class="ad_wrapper"]', + '//div[@id="article-sidebar"]', + '//div[@class="data-table-outer"]', + '//*[@class="story-date"]', + '//*[@class="story-header"]', + '//figure[contains(@class,"has-caption")]', + '//*[@class="story-related"]', + '//*[contains(@class, "byline")]', + '//p[contains(@class, "media-message")]', + '//*[contains(@class, "story-feature")]', + '//*[@id="video-carousel-container"]', + '//*[@id="also-related-links"]', + '//*[contains(@class, "share") or contains(@class, "hidden") or contains(@class, "hyper")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.bdgest.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.bdgest.com.php new file mode 100644 index 0000000..ffb13ac --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.bdgest.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bdgest.com/chronique-6027-BD-Adrastee-Tome-2.html', + 'body' => array( + '//*[contains(@class, "chronique")]', + ), + 'strip' => array( + '//*[contains(@class, "post-review")]', + '//*[contains(@class, "footer-review")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.bgr.in.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.bgr.in.php new file mode 100644 index 0000000..ee95495 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.bgr.in.php @@ -0,0 +1,24 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bgr.in/news/xiaomi-redmi-3-with-13-megapixel-camera-snapdragon-616-launched-price-specifications-and-features/', + 'body' => array( + '//div[@class="article-content"]', + ), + 'strip' => array( + '//*[@class="article-meta"]', + '//*[@class="contentAdsense300"]', + '//*[@class="iwpl-social-hide"]', + '//iframe[@class="iframeads"]', + '//*[@class="disqus_thread"]', + '//*[@class="outb-mobile OUTBRAIN"]', + '//*[@class="wdt_smart_alerts"]', + '//*[@class="footnote"]', + '//*[@id="gadget-widget"]', + '//header[@class="article-title entry-header"]', + ), + ), + ), +); \ No newline at end of file diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.businessweek.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.businessweek.com.php new file mode 100644 index 0000000..6f805f2 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.businessweek.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.businessweek.com/articles/2013-09-18/elon-musks-hyperloop-will-work-says-some-very-smart-software', + 'body' => array( + '//div[@id="lead_graphic"]', + '//div[@id="article_body"]', + ), + 'strip' => array( + '//*[contains(@class, "related_item")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.cnn.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.cnn.com.php new file mode 100644 index 0000000..a8f772f --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.cnn.com.php @@ -0,0 +1,25 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.cnn.com/2013/08/31/world/meast/syria-civil-war/index.html?hpt=hp_t1', + 'body' => array( + '//div[@class="cnn_strycntntlft"]', + ), + 'strip' => array( + '//div[@class="cnn_stryshrwdgtbtm"]', + '//div[@class="cnn_strybtmcntnt"]', + '//div[@class="cnn_strylftcntnt"]', + '//div[contains(@class, "cnnGalleryContainer")]', + '//div[contains(@class, "cnn_strylftcexpbx")]', + '//div[contains(@class, "articleGalleryNavContainer")]', + '//div[contains(@class, "cnnArticleGalleryCaptionControl")]', + '//div[contains(@class, "cnnArticleGalleryNavPrevNextDisabled")]', + '//div[contains(@class, "cnnArticleGalleryNavPrevNext")]', + '//div[contains(@class, "cnn_html_media_title_new")]', + '//div[contains(@id, "disqus")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.developpez.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.developpez.com.php new file mode 100644 index 0000000..cc7e125 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.developpez.com.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.developpez.com/actu/81757/Mozilla-annonce-la-disponibilite-de-Firefox-36-qui-passe-au-HTTP-2-et-permet-la-synchronisation-de-son-ecran-d-accueil/', + 'body' => array( + '//*[@itemprop="articleBody"]', + ), + 'strip' => array( + '//form', + '//div[@class="content"]/img', + '//a[last()]/following-sibling::*', + '//*[contains(@class,"actuTitle")]', + '//*[contains(@class,"date")]', + '//*[contains(@class,"inlineimg")]', + '//*[@id="signaler"]', + '//*[@id="signalerFrame"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.egscomics.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.egscomics.com.php new file mode 100644 index 0000000..49befe1 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.egscomics.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.egscomics.com/index.php?id=1690', + 'title' => '/html/head/title', + 'body' => array( + '//img[@id="comic"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.fakingnews.firstpost.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.fakingnews.firstpost.com.php new file mode 100644 index 0000000..4ea4166 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.fakingnews.firstpost.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.fakingnews.firstpost.com/2016/01/engineering-student-creates-record-in-a-decade-becomes-the-first-to-completely-exhaust-ball-pen-refill/', + 'body' => array( + '//div[@class="entry"]', + ), + 'strip' => array( + '//*[@class="socialshare_bar"]', + '//*[@class="authorbox"]', + '//*[@class="cf5_rps"]', + '//*[@class="60563 fb-comments fb-social-plugin"]', + ), + ), + ), +); \ No newline at end of file diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.forbes.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.forbes.com.php new file mode 100644 index 0000000..2167ff9 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.forbes.com.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.forbes.com/sites/andygreenberg/2013/09/05/follow-the-bitcoins-how-we-got-busted-buying-drugs-on-silk-roads-black-market/', + 'body' => array( + '//div[@id="leftRail"]/div[contains(@class, body)]', + ), + 'strip' => array( + '//aside', + '//div[contains(@class, "entity_block")]', + '//div[contains(@class, "vestpocket") and not contains(@class, "body")]', + '//div[contains(@style, "display")]', + '//div[contains(@id, "comment")]', + '//div[contains(@class, "widget")]', + '//div[contains(@class, "pagination")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.franceculture.fr.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.franceculture.fr.php new file mode 100644 index 0000000..f7ec0d8 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.franceculture.fr.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.franceculture.fr/emission-culture-eco-la-finance-aime-toujours-la-france-2016-01-08', + 'body' => array( + '//div[@class="text-zone"]', + ), + 'strip' => array( + '//ul[@class="tags"]', + ), + ) + ) +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.futura-sciences.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.futura-sciences.com.php new file mode 100644 index 0000000..7fe324b --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.futura-sciences.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.futura-sciences.com/magazines/espace/infos/actu/d/astronautique-curiosity-franchi-succes-dune-dingo-gap-52289/#xtor=RSS-8', + 'body' => array( + '//div[contains(@class, "content fiche-")]', + ), + 'strip' => array( + '//h1', + '//*[contains(@class, "content-date")]', + '//*[contains(@class, "diaporama")]', + '//*[contains(@class, "slider")]', + '//*[contains(@class, "cartouche")]', + '//*[contains(@class, "noprint")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.geekculture.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.geekculture.com.php new file mode 100644 index 0000000..7f03a1d --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.geekculture.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.geekculture.com/joyoftech/joyarchives/2180.html', + 'body' => array( + '//p[contains(@class,"Maintext")][2]/img', + ), + 'strip' => array(), + ), + ), +); + diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.howtogeek.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.howtogeek.com.php new file mode 100644 index 0000000..39ac0c5 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.howtogeek.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.howtogeek.com/235283/what-is-a-wireless-hard-drive-and-should-i-get-one/', + 'body' => array( + '//div[@class="thecontent"]', + ), + 'strip' => array( + '//*[@class="relatedside"]', + ), + ), + ), +); \ No newline at end of file diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.lepoint.fr.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.lepoint.fr.php new file mode 100644 index 0000000..f7386a5 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.lepoint.fr.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.lepoint.fr/c-est-arrive-aujourd-hui/19-septembre-1783-pour-la-premiere-fois-un-mouton-un-canard-et-un-coq-s-envoient-en-l-air-devant-louis-xvi-18-09-2012-1507704_494.php', + 'body' => array( + '//article', + ), + 'strip' => array( + '//*[contains(@class, "info_article")]', + '//*[contains(@class, "fildariane_titre")]', + '//*[contains(@class, "entete2_article")]', + '//*[contains(@class, "signature_article")]', + '//*[contains(@id, "share")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.lesnumeriques.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.lesnumeriques.com.php new file mode 100644 index 0000000..dfc0de6 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.lesnumeriques.com.php @@ -0,0 +1,26 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.lesnumeriques.com/blender/kitchenaid-diamond-5ksb1585-p27473/test.html', + 'body' => array( + '//*[@id="product-content"]', + '//*[@id="news-content"]', + '//*[@id="article-content"]', + ), + 'strip' => array( + '//form', + '//div[contains(@class, "price-v4"])', + '//div[contains(@class, "authors-and-date")]', + '//div[contains(@class, "mini-product")]', + '//div[@id="articles-related-authors"]', + '//div[@id="tags-socials"]', + '//div[@id="user-reviews"]', + '//div[@id="product-reviews"]', + '//div[@id="publication-breadcrumbs-and-date"]', + '//div[@id="publication-breadcrumbs-and-date"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.mac4ever.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.mac4ever.com.php new file mode 100644 index 0000000..a280bd3 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.mac4ever.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.mac4ever.com/actu/87392_video-quand-steve-jobs-et-bill-gates-jouaient-au-bachelor-avec-le-mac', + 'body' => array( + '//div[contains(@class, "news-news-content")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.makeuseof.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.makeuseof.com.php new file mode 100644 index 0000000..d5d3bf4 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.makeuseof.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.makeuseof.com/tag/having-problems-with-audio-in-windows-10-heres-a-likely-fix/', + 'body' => array( + '//div[@class="entry"]', + ), + 'strip' => array( + '//*[@class="new_sharebar"]', + '//*[@class="author"]', + '//*[@class="wdt_grouvi"]', + '//*[@class="wdt_smart_alerts"]', + '//*[@class="modal fade grouvi"]', + ), + ), + ), +); \ No newline at end of file diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.monsieur-le-chien.fr.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.monsieur-le-chien.fr.php new file mode 100644 index 0000000..5f5e987 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.monsieur-le-chien.fr.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.monsieur-le-chien.fr/index.php?planche=672', + 'body' => array( + '//img[starts-with(@src, "i/planches/")]', + ), + ) + ) +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.npr.org.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.npr.org.php new file mode 100644 index 0000000..0205d7d --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.npr.org.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.npr.org/blogs/thesalt/2013/09/17/223345977/auto-brewery-syndrome-apparently-you-can-make-beer-in-your-gut', + 'body' => array( + '//div[@id="storytext"]', + ), + 'strip' => array( + '//*[@class="bucket img"]', + '//*[@class="creditwrap"]', + '//div[@class="credit-caption"]', + '//*[@class="credit"]', + '//*[@class="captionwrap"]', + '//*[@class="toggle-caption"]', + '//*[contains(@class, "enlargebtn")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.numerama.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.numerama.com.php new file mode 100644 index 0000000..d18ed1c --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.numerama.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.numerama.com/sciences/125959-recherches-ladn-recompensees-nobel-de-chimie.html', + 'body' => array( + '//article', + ), + 'strip' => array( + '//footer', + '//section[@class="related-article"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.oneindia.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.oneindia.com.php new file mode 100644 index 0000000..810f138 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.oneindia.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.oneindia.com/india/b-luru-govt-likely-remove-word-eunuch-from-sec-36-a-karnataka-police-act-1981173.html', + 'body' => array( + '//div[@class="ecom-ad-content"]', + ), + 'strip' => array( + '//*[@id="view_cmtns"]', + ), + ), + ), +); \ No newline at end of file diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.pcinpact.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.pcinpact.com.php new file mode 100644 index 0000000..24e9a2f --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.pcinpact.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.pcinpact.com/news/85954-air-france-ne-vous-demande-plus-deteindre-vos-appareils-electroniques.htm?utm_source=PCi_RSS_Feed&utm_medium=news&utm_campaign=pcinpact', + 'body' => array( + '//div[contains(@id, "actu_content")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.pseudo-sciences.org.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.pseudo-sciences.org.php new file mode 100644 index 0000000..017f916 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.pseudo-sciences.org.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.pseudo-sciences.org/spip.php?article2275', + 'body' => array( + '//div[@id="art_main"]', + ), + 'strip' => array( + '//div[@id="art_print"]', + '//div[@id="art_chapo"]', + '//img[@class="puce"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.sciencemag.org.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.sciencemag.org.php new file mode 100644 index 0000000..ae7a93a --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.sciencemag.org.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.sciencemag.org/news/2016/01/could-bright-foamy-wak$', + 'body' => array( + '//div[@class="row--hero"]', + '//article[contains(@class,"primary")]', + ), + 'strip' => array( + '//header[@class="article__header"]', + '//footer[@class="article__foot"]', + ), + ), + ) +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.slate.fr.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.slate.fr.php new file mode 100644 index 0000000..3591f39 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.slate.fr.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.slate.fr/monde/77034/allemagne-2013-couacs-campagne', + 'body' => array( + '//div[@class="article_content"]', + ), + 'strip' => array( + '//*[@id="slate_associated_bn"]', + '//*[@id="ligatus-article"]', + '//*[@id="article_sidebar"]', + '//div[contains(@id, "reseaux")]', + '//*[contains(@class, "smart") or contains(@class, "article_tags") or contains(@class, "article_reactions")]', + '//*[contains(@class, "OUTBRAIN") or contains(@class, "related_item") or contains(@class, "share")]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.universfreebox.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.universfreebox.com.php new file mode 100644 index 0000000..fe9b54a --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.universfreebox.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.universfreebox.com/article/24305/4G-Bouygues-Telecom-lance-une-vente-flash-sur-son-forfait-Sensation-3Go', + 'body' => array( + '//div[@id="corps_corps"]', + ), + 'strip' => array( + '//*[@id="formulaire"]', + '//*[@id="commentaire"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.zeit.de.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.zeit.de.php new file mode 100644 index 0000000..cdac1cb --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/www.zeit.de.php @@ -0,0 +1,41 @@ + array( + '%^/zeit-magazin.*%' => array( + 'test_url' => 'http://www.zeit.de/zeit-magazin/2015/15/pegida-kathrin-oertel-lutz-bachmann', + 'body' => array( + '//article[@class="article"]', + ), + 'strip' => array( + '//header/div/h1', + '//header/div/div[@class="article__head__subtitle"]', + '//header/div/div[@class="article__column__author"]', + '//header/div/div[@class="article__column__author"]', + '//header/div/span[@class="article__head__meta-wrap"]', + '//form', + '//style', + '//div[contains(@class, "ad-tile")]', + '//div[@class="iqd-mobile-adplace"]', + '//div[@id="iq-artikelanker"]', + '//div[@id="js-social-services"]', + '//section[@id="js-comments"]', + '//aside', + ), + ), + '%.*%' => array( + 'test_url' => 'http://www.zeit.de/politik/ausland/2015-04/thessaloniki-krise-griechenland-yannis-boutaris/', + 'body' => array( + '//div[@class="article-body"]', + ), + 'strip' => array( + '//*[@class="articleheader"]', + '//*[@class="excerpt"]', + '//div[contains(@class, "ad")]', + '//div[@itemprop="video"]', + '//*[@class="articlemeta"]', + '//*[@class="articlemeta-clear"]', + '//*[@class="zol_inarticletools"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/xkcd.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/xkcd.com.php new file mode 100644 index 0000000..7a04bec --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/xkcd.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%alt="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Rules/zdnet.com.php b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/zdnet.com.php new file mode 100644 index 0000000..45fa3cc --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Rules/zdnet.com.php @@ -0,0 +1,24 @@ + array( + '%.*%' => array( + 'test_url' => 'http://zdnet.com.feedsportal.com/c/35462/f/675637/s/4a33c93e/sc/11/l/0L0Szdnet0N0Carticle0Cchina0Eus0Eagree0Eon0Ecybercrime0Ecooperation0Eamid0Econtinued0Etension0C0Tftag0FRSSbaffb68/story01.htm', + 'body' => array( + '//p[@class="summary"]', + '//div[contains(@class,"storyBody")]', + ), + 'strip' => array( + '//*[contains(@class,"ad-")]', + '//p/span', + '//script', + '//p[@class="summary"]', + '//div[contains(@class,"relatedContent")]', + '//div[contains(@class,"loader")]', + '//p[@class="photoDetails"]', + '//div[@class="thumbnailSlider"]', + '//div[@class="shortcodeGalleryWrapper"]', + ), + ), + ), +); diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Scraper/CandidateParser.php b/vendor/fguillot/picofeed/lib/PicoFeed/Scraper/CandidateParser.php new file mode 100644 index 0000000..802b01b --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Scraper/CandidateParser.php @@ -0,0 +1,273 @@ +dom = XmlParser::getHtmlDocument(''.$html); + $this->xpath = new DOMXPath($this->dom); + } + + /** + * Get the relevant content with the list of potential attributes. + * + * @return string + */ + public function execute() + { + $content = $this->findContentWithCandidates(); + + if (strlen($content) < 200) { + $content = $this->findContentWithArticle(); + } + + if (strlen($content) < 50) { + $content = $this->findContentWithBody(); + } + + return $this->stripGarbage($content); + } + + /** + * Find content based on the list of tag candidates. + * + * @return string + */ + public function findContentWithCandidates() + { + foreach ($this->candidatesAttributes as $candidate) { + Logger::setMessage(get_called_class().': Try this candidate: "'.$candidate.'"'); + + $nodes = $this->xpath->query('//*[(contains(@class, "'.$candidate.'") or @id="'.$candidate.'") and not (contains(@class, "nav") or contains(@class, "page"))]'); + + if ($nodes !== false && $nodes->length > 0) { + Logger::setMessage(get_called_class().': Find candidate "'.$candidate.'"'); + + return $this->dom->saveXML($nodes->item(0)); + } + } + + return ''; + } + + /** + * Find
tag. + * + * @return string + */ + public function findContentWithArticle() + { + $nodes = $this->xpath->query('//article'); + + if ($nodes !== false && $nodes->length > 0) { + Logger::setMessage(get_called_class().': Find
tag'); + + return $this->dom->saveXML($nodes->item(0)); + } + + return ''; + } + + /** + * Find tag. + * + * @return string + */ + public function findContentWithBody() + { + $nodes = $this->xpath->query('//body'); + + if ($nodes !== false && $nodes->length > 0) { + Logger::setMessage(get_called_class().' Find '); + + return $this->dom->saveXML($nodes->item(0)); + } + + return ''; + } + + /** + * Strip useless tags. + * + * @param string $content + * + * @return string + */ + public function stripGarbage($content) + { + $dom = XmlParser::getDomDocument($content); + + if ($dom !== false) { + $xpath = new DOMXPath($dom); + + $this->stripTags($xpath); + $this->stripAttributes($dom, $xpath); + + $content = $dom->saveXML($dom->documentElement); + } + + return $content; + } + + /** + * Remove blacklisted tags. + * + * @param DOMXPath $xpath + */ + public function stripTags(DOMXPath $xpath) + { + foreach ($this->stripTags as $tag) { + $nodes = $xpath->query('//'.$tag); + + if ($nodes !== false && $nodes->length > 0) { + Logger::setMessage(get_called_class().': Strip tag: "'.$tag.'"'); + + foreach ($nodes as $node) { + $node->parentNode->removeChild($node); + } + } + } + } + + /** + * Remove blacklisted attributes. + * + * @param DomDocument $dom + * @param DOMXPath $xpath + */ + public function stripAttributes(DomDocument $dom, DOMXPath $xpath) + { + foreach ($this->stripAttributes as $attribute) { + $nodes = $xpath->query('//*[contains(@class, "'.$attribute.'") or contains(@id, "'.$attribute.'")]'); + + if ($nodes !== false && $nodes->length > 0) { + Logger::setMessage(get_called_class().': Strip attribute: "'.$attribute.'"'); + + foreach ($nodes as $node) { + if ($this->shouldRemove($dom, $node)) { + $node->parentNode->removeChild($node); + } + } + } + } + } + + /** + * Return false if the node should not be removed. + * + * @param DomDocument $dom + * @param DomNode $node + * + * @return bool + */ + public function shouldRemove(DomDocument $dom, $node) + { + $document_length = strlen($dom->textContent); + $node_length = strlen($node->textContent); + + if ($document_length === 0) { + return true; + } + + $ratio = $node_length * 100 / $document_length; + + if ($ratio >= 90) { + Logger::setMessage(get_called_class().': Should not remove this node ('.$node->nodeName.') ratio: '.$ratio.'%'); + + return false; + } + + return true; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Scraper/ParserInterface.php b/vendor/fguillot/picofeed/lib/PicoFeed/Scraper/ParserInterface.php new file mode 100644 index 0000000..a33fefc --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Scraper/ParserInterface.php @@ -0,0 +1,13 @@ +getRulesFileList($hostname); + + foreach ($this->getRulesFolders() as $folder) { + $rule = $this->loadRuleFile($folder, $files); + + if (!empty($rule)) { + return $rule; + } + } + } + + return array(); + } + + /** + * Get the list of possible rules file names for a given hostname. + * + * @param string $hostname Hostname + * + * @return array + */ + public function getRulesFileList($hostname) + { + $files = array($hostname); // subdomain.domain.tld + $parts = explode('.', $hostname); + $len = count($parts); + + if ($len > 2) { + $subdomain = array_shift($parts); + $files[] = implode('.', $parts); // domain.tld + $files[] = '.'.implode('.', $parts); // .domain.tld + $files[] = $subdomain; // subdomain + } elseif ($len === 2) { + $files[] = '.'.implode('.', $parts); // .domain.tld + $files[] = $parts[0]; // domain + } + + return $files; + } + + /** + * Load a rule file from the defined folder. + * + * @param string $folder Rule directory + * @param array $files List of possible file names + * + * @return array + */ + public function loadRuleFile($folder, array $files) + { + foreach ($files as $file) { + $filename = $folder.'/'.$file.'.php'; + if (file_exists($filename)) { + Logger::setMessage(get_called_class().' Load rule: '.$file); + + return include $filename; + } + } + + return array(); + } + + /** + * Get the list of folders that contains rules. + * + * @return array + */ + public function getRulesFolders() + { + $folders = array(); + + if ($this->config !== null && $this->config->getGrabberRulesFolder() !== null) { + $folders[] = $this->config->getGrabberRulesFolder(); + } + + $folders[] = __DIR__ . '/../Rules'; + + return $folders; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Scraper/RuleParser.php b/vendor/fguillot/picofeed/lib/PicoFeed/Scraper/RuleParser.php new file mode 100644 index 0000000..95665bf --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Scraper/RuleParser.php @@ -0,0 +1,83 @@ +rules = $rules; + $this->dom = XmlParser::getHtmlDocument(''.$html); + $this->xpath = new DOMXPath($this->dom); + } + + /** + * Get the relevant content with predefined rules. + * + * @return string + */ + public function execute() + { + $this->stripTags(); + + return $this->findContent(); + } + + /** + * Remove HTML tags. + */ + public function stripTags() + { + if (isset($this->rules['strip']) && is_array($this->rules['strip'])) { + foreach ($this->rules['strip'] as $pattern) { + $nodes = $this->xpath->query($pattern); + + if ($nodes !== false && $nodes->length > 0) { + foreach ($nodes as $node) { + $node->parentNode->removeChild($node); + } + } + } + } + } + + /** + * Fetch content based on Xpath rules. + */ + public function findContent() + { + $content = ''; + + if (isset($this->rules['body']) && is_array($this->rules['body'])) { + foreach ($this->rules['body'] as $pattern) { + $nodes = $this->xpath->query($pattern); + + if ($nodes !== false && $nodes->length > 0) { + foreach ($nodes as $node) { + $content .= $this->dom->saveXML($node); + } + } + } + } + + return $content; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Scraper/Scraper.php b/vendor/fguillot/picofeed/lib/PicoFeed/Scraper/Scraper.php new file mode 100644 index 0000000..980a88d --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Scraper/Scraper.php @@ -0,0 +1,267 @@ +enableCandidateParser = false; + return $this; + } + + /** + * Get encoding. + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Set encoding. + * + * @param string $encoding + * + * @return Scraper + */ + public function setEncoding($encoding) + { + $this->encoding = $encoding; + + return $this; + } + + /** + * Get URL to download. + * + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set URL to download. + * + * @param string $url URL + * + * @return Scraper + */ + public function setUrl($url) + { + $this->url = $url; + + return $this; + } + + /** + * Return true if the scraper found relevant content. + * + * @return bool + */ + public function hasRelevantContent() + { + return !empty($this->content); + } + + /** + * Get relevant content. + * + * @return string + */ + public function getRelevantContent() + { + return $this->content; + } + + /** + * Get raw content (unfiltered). + * + * @return string + */ + public function getRawContent() + { + return $this->html; + } + + /** + * Set raw content (unfiltered). + * + * @param string $html + * + * @return Scraper + */ + public function setRawContent($html) + { + $this->html = $html; + + return $this; + } + + /** + * Get filtered relevant content. + * + * @return string + */ + public function getFilteredContent() + { + $filter = Filter::html($this->content, $this->url); + $filter->setConfig($this->config); + + return $filter->execute(); + } + + /** + * Download the HTML content. + * + * @return bool + */ + public function download() + { + if (!empty($this->url)) { + + // Clear everything + $this->html = ''; + $this->content = ''; + $this->encoding = ''; + + try { + $client = Client::getInstance(); + $client->setConfig($this->config); + $client->setTimeout($this->config->getGrabberTimeout()); + $client->setUserAgent($this->config->getGrabberUserAgent()); + $client->execute($this->url); + + $this->url = $client->getUrl(); + $this->html = $client->getContent(); + $this->encoding = $client->getEncoding(); + + return true; + } catch (ClientException $e) { + Logger::setMessage(get_called_class().': '.$e->getMessage()); + } + } + + return false; + } + + /** + * Execute the scraper. + */ + public function execute() + { + $this->content = ''; + $this->html = ''; + $this->encoding = ''; + + $this->download(); + $this->prepareHtml(); + + $parser = $this->getParser(); + + if ($parser !== null) { + $this->content = $parser->execute(); + Logger::setMessage(get_called_class().': Content length: '.strlen($this->content).' bytes'); + } + } + + /** + * Get the parser. + * + * @return ParserInterface + */ + public function getParser() + { + $ruleLoader = new RuleLoader($this->config); + $rules = $ruleLoader->getRules($this->url); + + if (!empty($rules['grabber'])) { + Logger::setMessage(get_called_class().': Parse content with rules'); + + foreach ($rules['grabber'] as $pattern => $rule) { + $url = new Url($this->url); + $sub_url = $url->getFullPath(); + + if (preg_match($pattern, $sub_url)) { + Logger::setMessage(get_called_class().': Matched url '.$sub_url); + return new RuleParser($this->html, $rule); + } + } + } elseif ($this->enableCandidateParser) { + Logger::setMessage(get_called_class().': Parse content with candidates'); + } + + return new CandidateParser($this->html); + } + + /** + * Normalize encoding and strip head tag. + */ + public function prepareHtml() + { + $html_encoding = XmlParser::getEncodingFromMetaTag($this->html); + + $this->html = Encoding::convert($this->html, $html_encoding ?: $this->encoding); + $this->html = Filter::stripHeadTags($this->html); + + Logger::setMessage(get_called_class().': HTTP Encoding "'.$this->encoding.'" ; HTML Encoding "'.$html_encoding.'"'); + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Serialization/Subscription.php b/vendor/fguillot/picofeed/lib/PicoFeed/Serialization/Subscription.php new file mode 100644 index 0000000..12eccfd --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Serialization/Subscription.php @@ -0,0 +1,175 @@ +title = $title; + return $this; + } + + /** + * Get title + * + * @access public + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Set feed URL + * + * @access public + * @param string $feedUrl + * @return Subscription + */ + public function setFeedUrl($feedUrl) + { + $this->feedUrl = $feedUrl; + return $this; + } + + /** + * Get feed URL + * + * @access public + * @return string + */ + public function getFeedUrl() + { + return $this->feedUrl; + } + + /** + * Set site URL + * + * @access public + * @param string $siteUrl + * @return Subscription + */ + public function setSiteUrl($siteUrl) + { + $this->siteUrl = $siteUrl; + return $this; + } + + /** + * Get site URL + * + * @access public + * @return string + */ + public function getSiteUrl() + { + return $this->siteUrl; + } + + /** + * Set category + * + * @access public + * @param string $category + * @return Subscription + */ + public function setCategory($category) + { + $this->category = $category; + return $this; + } + + /** + * Get category + * + * @access public + * @return string + */ + public function getCategory() + { + return $this->category; + } + + /** + * Set description + * + * @access public + * @param string $description + * @return Subscription + */ + public function setDescription($description) + { + $this->description = $description; + return $this; + } + + /** + * Get description + * + * @access public + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Set type + * + * @access public + * @param string $type + * @return Subscription + */ + public function setType($type) + { + $this->type = $type; + return $this; + } + + /** + * Get type + * + * @access public + * @return string + */ + public function getType() + { + return $this->type; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionList.php b/vendor/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionList.php new file mode 100644 index 0000000..b173f89 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionList.php @@ -0,0 +1,75 @@ +title = $title; + return $this; + } + + /** + * Get title + * + * @access public + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Add subscription + * + * @access public + * @param Subscription $subscription + * @return SubscriptionList + */ + public function addSubscription(Subscription $subscription) + { + $this->subscriptions[] = $subscription; + return $this; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionListBuilder.php b/vendor/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionListBuilder.php new file mode 100644 index 0000000..838e4cb --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionListBuilder.php @@ -0,0 +1,204 @@ +subscriptionList = $subscriptionList; + } + + /** + * Get object instance + * + * @static + * @access public + * @param SubscriptionList $subscriptionList + * @return SubscriptionListBuilder + */ + public static function create(SubscriptionList $subscriptionList) + { + return new static($subscriptionList); + } + + /** + * Build OPML feed + * + * @access public + * @param string $filename + * @return string + */ + public function build($filename = '') + { + $this->document = new DomDocument('1.0', 'UTF-8'); + $this->document->formatOutput = true; + + $opmlElement = $this->document->createElement('opml'); + $opmlElement->setAttribute('version', '1.0'); + + $headElement = $this->document->createElement('head'); + + if ($this->subscriptionList->getTitle() !== '') { + $titleElement = $this->document->createElement('title'); + $titleElement->appendChild($this->document->createTextNode($this->subscriptionList->getTitle())); + $headElement->appendChild($titleElement); + } + + $opmlElement->appendChild($headElement); + $opmlElement->appendChild($this->buildBody()); + $this->document->appendChild($opmlElement); + + if ($filename !== '') { + $this->document->save($filename); + return ''; + } + + return $this->document->saveXML(); + } + + /** + * Return true if the list has categories + * + * @access public + * @return bool + */ + public function hasCategories() + { + foreach ($this->subscriptionList->subscriptions as $subscription) { + if ($subscription->getCategory() !== '') { + return true; + } + } + + return false; + } + + /** + * Build OPML body + * + * @access protected + * @return DOMElement + */ + protected function buildBody() + { + $bodyElement = $this->document->createElement('body'); + + if ($this->hasCategories()) { + $this->buildCategories($bodyElement); + return $bodyElement; + } + + foreach ($this->subscriptionList->subscriptions as $subscription) { + $bodyElement->appendChild($this->buildSubscription($subscription)); + } + + return $bodyElement; + } + + /** + * Build categories section + * + * @access protected + * @param DOMElement $bodyElement + */ + protected function buildCategories(DOMElement $bodyElement) + { + $categories = $this->groupByCategories(); + + foreach ($categories as $category => $subscriptions) { + $bodyElement->appendChild($this->buildCategory($category, $subscriptions)); + } + } + + /** + * Build category tag + * + * @access protected + * @param string $category + * @param array $subscriptions + * @return DOMElement + */ + protected function buildCategory($category, array $subscriptions) + { + $outlineElement = $this->document->createElement('outline'); + $outlineElement->setAttribute('text', $category); + + foreach ($subscriptions as $subscription) { + $outlineElement->appendChild($this->buildSubscription($subscription)); + } + + return $outlineElement; + } + + /** + * Build subscription entry + * + * @access public + * @param Subscription $subscription + * @return DOMElement + */ + protected function buildSubscription(Subscription $subscription) + { + $outlineElement = $this->document->createElement('outline'); + $outlineElement->setAttribute('type', $subscription->getType() ?: 'rss'); + $outlineElement->setAttribute('text', $subscription->getTitle() ?: $subscription->getFeedUrl()); + $outlineElement->setAttribute('xmlUrl', $subscription->getFeedUrl()); + + if ($subscription->getTitle() !== '') { + $outlineElement->setAttribute('title', $subscription->getTitle()); + } + + if ($subscription->getDescription() !== '') { + $outlineElement->setAttribute('description', $subscription->getDescription()); + } + + if ($subscription->getSiteUrl() !== '') { + $outlineElement->setAttribute('htmlUrl', $subscription->getSiteUrl()); + } + + return $outlineElement; + } + + /** + * Group subscriptions by category + * + * @access private + * @return array + */ + private function groupByCategories() + { + $categories = array(); + + foreach ($this->subscriptionList->subscriptions as $subscription) { + $categories[$subscription->getCategory()][] = $subscription; + } + + return $categories; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionListParser.php b/vendor/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionListParser.php new file mode 100644 index 0000000..9085588 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionListParser.php @@ -0,0 +1,100 @@ +subscriptionList = new SubscriptionList(); + $this->data = trim($data); + } + + /** + * Get object instance + * + * @static + * @access public + * @param string $data + * @return SubscriptionListParser + */ + public static function create($data) + { + return new static($data); + } + + /** + * Parse a subscription list entry + * + * @access public + * @throws MalformedXmlException + * @return SubscriptionList + */ + public function parse() + { + $xml = XmlParser::getSimpleXml($this->data); + + if (! $xml || !isset($xml->head) || !isset($xml->body)) { + throw new MalformedXmlException('Unable to parse OPML file: invalid XML'); + } + + $this->parseTitle($xml->head); + $this->parseEntries($xml->body); + + return $this->subscriptionList; + } + + /** + * Parse title + * + * @access protected + * @param SimpleXMLElement $xml + */ + protected function parseTitle(SimpleXMLElement $xml) + { + $this->subscriptionList->setTitle((string) $xml->title); + } + + /** + * Parse entries + * + * @access protected + * @param SimpleXMLElement $body + */ + private function parseEntries(SimpleXMLElement $body) + { + foreach ($body->outline as $outlineElement) { + if (isset($outlineElement->outline)) { + $this->parseEntries($outlineElement); + } else { + $this->subscriptionList->subscriptions[] = SubscriptionParser::create($body, $outlineElement)->parse(); + } + } + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionParser.php b/vendor/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionParser.php new file mode 100644 index 0000000..caff07c --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionParser.php @@ -0,0 +1,142 @@ +parentElement = $parentElement; + $this->outlineElement = $outlineElement; + $this->subscription = new Subscription(); + } + + /** + * Get object instance + * + * @static + * @access public + * @param SimpleXMLElement $parentElement + * @param SimpleXMLElement $outlineElement + * @return SubscriptionParser + */ + public static function create(SimpleXMLElement $parentElement, SimpleXMLElement $outlineElement) + { + return new static($parentElement, $outlineElement); + } + + /** + * Parse subscription entry + * + * @access public + * @return Subscription + */ + public function parse() + { + $this->subscription->setCategory($this->findCategory()); + $this->subscription->setTitle($this->findTitle()); + $this->subscription->setFeedUrl($this->findFeedUrl()); + $this->subscription->setSiteUrl($this->findSiteUrl()); + $this->subscription->setType($this->findType()); + $this->subscription->setDescription($this->findDescription()); + + return $this->subscription; + } + + /** + * Find category. + * + * @access protected + * @return string + */ + protected function findCategory() + { + return isset($this->parentElement['text']) ? (string) $this->parentElement['text'] : ''; + } + + /** + * Find title. + * + * @access protected + * @return string + */ + protected function findTitle() + { + return isset($this->outlineElement['title']) ? (string) $this->outlineElement['title'] : (string) $this->outlineElement['text']; + } + + /** + * Find feed url. + * + * @access protected + * @return string + */ + protected function findFeedUrl() + { + return (string) $this->outlineElement['xmlUrl']; + } + + /** + * Find site url. + * + * @access protected + * @return string + */ + protected function findSiteUrl() + { + return isset($this->outlineElement['htmlUrl']) ? (string) $this->outlineElement['htmlUrl'] : $this->findFeedUrl(); + } + + /** + * Find type. + * + * @access protected + * @return string + */ + protected function findType() + { + return isset($this->outlineElement['version']) ? (string) $this->outlineElement['version'] : + isset($this->outlineElement['type']) ? (string) $this->outlineElement['type'] : 'rss'; + } + + /** + * Find description. + * + * @access protected + * @return string + */ + protected function findDescription() + { + return isset($this->outlineElement['description']) ? (string) $this->outlineElement['description'] : $this->findTitle(); + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/AtomFeedBuilder.php b/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/AtomFeedBuilder.php new file mode 100644 index 0000000..9d80d71 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/AtomFeedBuilder.php @@ -0,0 +1,65 @@ +helper = new AtomHelper($this->getDocument()); + + $this->feedElement = $this->getDocument()->createElement('feed'); + $this->feedElement->setAttributeNodeNS(new DomAttr('xmlns', 'http://www.w3.org/2005/Atom')); + + $generator = $this->getDocument()->createElement('generator', 'PicoFeed'); + $generator->setAttribute('uri', 'https://github.com/fguillot/picoFeed'); + $this->feedElement->appendChild($generator); + + $this->helper + ->buildTitle($this->feedElement, $this->feedTitle) + ->buildId($this->feedElement, $this->feedUrl) + ->buildDate($this->feedElement, $this->feedDate) + ->buildLink($this->feedElement, $this->siteUrl) + ->buildLink($this->feedElement, $this->feedUrl, 'self', 'application/atom+xml') + ->buildAuthor($this->feedElement, $this->authorName, $this->authorEmail, $this->authorUrl) + ; + + foreach ($this->items as $item) { + $this->feedElement->appendChild($item->build()); + } + + $this->getDocument()->appendChild($this->feedElement); + + if ($filename !== '') { + $this->getDocument()->save($filename); + } + + return $this->getDocument()->saveXML(); + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/AtomHelper.php b/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/AtomHelper.php new file mode 100644 index 0000000..def6b0b --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/AtomHelper.php @@ -0,0 +1,139 @@ +document = $document; + } + + /** + * Build node + * + * @access public + * @param DOMElement $element + * @param string $tag + * @param string $value + * @return AtomHelper + */ + public function buildNode(DOMElement $element, $tag, $value) + { + $node = $this->document->createElement($tag); + $node->appendChild($this->document->createTextNode($value)); + $element->appendChild($node); + return $this; + } + + /** + * Build title + * + * @access public + * @param DOMElement $element + * @param string $title + * @return AtomHelper + */ + public function buildTitle(DOMElement $element, $title) + { + return $this->buildNode($element, 'title', $title); + } + + /** + * Build id + * + * @access public + * @param DOMElement $element + * @param string $id + * @return AtomHelper + */ + public function buildId(DOMElement $element, $id) + { + return $this->buildNode($element, 'id', $id); + } + + /** + * Build date element + * + * @access public + * @param DOMElement $element + * @param DateTime $date + * @param string $type + * @return AtomHelper + */ + public function buildDate(DOMElement $element, DateTime $date, $type = 'updated') + { + return $this->buildNode($element, $type, $date->format(DateTime::ATOM)); + } + + /** + * Build link element + * + * @access public + * @param DOMElement $element + * @param string $url + * @param string $rel + * @param string $type + * @return AtomHelper + */ + public function buildLink(DOMElement $element, $url, $rel = 'alternate', $type = 'text/html') + { + $node = $this->document->createElement('link'); + $node->setAttribute('rel', $rel); + $node->setAttribute('type', $type); + $node->setAttribute('href', $url); + $element->appendChild($node); + + return $this; + } + + /** + * Build author element + * + * @access public + * @param DOMElement $element + * @param string $authorName + * @param string $authorEmail + * @param string $authorUrl + * @return AtomHelper + */ + public function buildAuthor(DOMElement $element, $authorName, $authorEmail, $authorUrl) + { + if (!empty($authorName)) { + $author = $this->document->createElement('author'); + $this->buildNode($author, 'name', $authorName); + + if (!empty($authorEmail)) { + $this->buildNode($author, 'email', $authorEmail); + } + + if (!empty($authorUrl)) { + $this->buildNode($author, 'uri', $authorUrl); + } + + $element->appendChild($author); + } + + return $this; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/AtomItemBuilder.php b/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/AtomItemBuilder.php new file mode 100644 index 0000000..dfdfe68 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/AtomItemBuilder.php @@ -0,0 +1,63 @@ +itemElement = $this->feedBuilder->getDocument()->createElement('entry'); + $this->helper = new AtomHelper($this->feedBuilder->getDocument()); + + if (!empty($this->itemId)) { + $this->helper->buildId($this->itemElement, $this->itemId); + } else { + $this->helper->buildId($this->itemElement, $this->itemUrl); + } + + $this->helper + ->buildTitle($this->itemElement, $this->itemTitle) + ->buildLink($this->itemElement, $this->itemUrl) + ->buildDate($this->itemElement, $this->itemUpdatedDate, 'updated') + ->buildDate($this->itemElement, $this->itemPublishedDate, 'published') + ->buildAuthor($this->itemElement, $this->authorName, $this->authorEmail, $this->authorUrl) + ; + + if (!empty($this->itemSummary)) { + $this->helper->buildNode($this->itemElement, 'summary', $this->itemSummary); + } + + if (!empty($this->itemContent)) { + $node = $this->feedBuilder->getDocument()->createElement('content'); + $node->setAttribute('type', 'html'); + $node->appendChild($this->feedBuilder->getDocument()->createCDATASection($this->itemContent)); + $this->itemElement->appendChild($node); + } + + return $this->itemElement; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/FeedBuilder.php b/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/FeedBuilder.php new file mode 100644 index 0000000..e7bdbc7 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/FeedBuilder.php @@ -0,0 +1,185 @@ +document = new DomDocument('1.0', 'UTF-8'); + $this->document->formatOutput = true; + } + + /** + * Get new object instance + * + * @access public + * @return static + */ + public static function create() + { + return new static(); + } + + /** + * Add feed title + * + * @access public + * @param string $title + * @return $this + */ + public function withTitle($title) + { + $this->feedTitle = $title; + return $this; + } + + /** + * Add feed url + * + * @access public + * @param string $url + * @return $this + */ + public function withFeedUrl($url) + { + $this->feedUrl = $url; + return $this; + } + + /** + * Add website url + * + * @access public + * @param string $url + * @return $this + */ + public function withSiteUrl($url) + { + $this->siteUrl = $url; + return $this; + } + + /** + * Add feed date + * + * @access public + * @param DateTime $date + * @return $this + */ + public function withDate(DateTime $date) + { + $this->feedDate = $date; + return $this; + } + + /** + * Add feed author + * + * @access public + * @param string $name + * @param string $email + * @param string $url + * @return $this + */ + public function withAuthor($name, $email = '', $url ='') + { + $this->authorName = $name; + $this->authorEmail = $email; + $this->authorUrl = $url; + return $this; + } + + /** + * Add feed item + * + * @access public + * @param ItemBuilder $item + * @return $this + */ + public function withItem(ItemBuilder $item) + { + $this->items[] = $item; + return $this; + } + + /** + * Get DOM document + * + * @access public + * @return DOMDocument + */ + public function getDocument() + { + return $this->document; + } + + /** + * Build feed + * + * @abstract + * @access public + * @param string $filename + * @return string + */ + abstract public function build($filename = ''); +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/ItemBuilder.php b/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/ItemBuilder.php new file mode 100644 index 0000000..86985bc --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/ItemBuilder.php @@ -0,0 +1,209 @@ +feedBuilder = $feedBuilder; + } + + /** + * Get new object instance + * + * @access public + * @param FeedBuilder $feedBuilder + * @return static + */ + public static function create(FeedBuilder $feedBuilder) + { + return new static($feedBuilder); + } + + /** + * Add item title + * + * @access public + * @param string $title + * @return $this + */ + public function withTitle($title) + { + $this->itemTitle = $title; + return $this; + } + + /** + * Add item id + * + * @access public + * @param string $id + * @return $this + */ + public function withId($id) + { + $this->itemId = $id; + return $this; + } + + /** + * Add item url + * + * @access public + * @param string $url + * @return $this + */ + public function withUrl($url) + { + $this->itemUrl = $url; + return $this; + } + + /** + * Add item summary + * + * @access public + * @param string $summary + * @return $this + */ + public function withSummary($summary) + { + $this->itemSummary = $summary; + return $this; + } + + /** + * Add item content + * + * @access public + * @param string $content + * @return $this + */ + public function withContent($content) + { + $this->itemContent = $content; + return $this; + } + + /** + * Add item updated date + * + * @access public + * @param DateTime $date + * @return $this + */ + public function withUpdatedDate(DateTime $date) + { + $this->itemUpdatedDate = $date; + return $this; + } + + /** + * Add item published date + * + * @access public + * @param DateTime $date + * @return $this + */ + public function withPublishedDate(DateTime $date) + { + $this->itemPublishedDate = $date; + return $this; + } + + /** + * Add item author + * + * @access public + * @param string $name + * @param string $email + * @param string $url + * @return $this + */ + public function withAuthor($name, $email = '', $url ='') + { + $this->authorName = $name; + $this->authorEmail = $email; + $this->authorUrl = $url; + return $this; + } + + /** + * Build item + * + * @abstract + * @access public + * @return DOMElement + */ + abstract public function build(); +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/Rss20FeedBuilder.php b/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/Rss20FeedBuilder.php new file mode 100644 index 0000000..d4ce160 --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/Rss20FeedBuilder.php @@ -0,0 +1,76 @@ +helper = new Rss20Helper($this->getDocument()); + + $this->rssElement = $this->getDocument()->createElement('rss'); + $this->rssElement->setAttribute('version', '2.0'); + $this->rssElement->setAttributeNodeNS(new DomAttr('xmlns:content', 'http://purl.org/rss/1.0/modules/content/')); + $this->rssElement->setAttributeNodeNS(new DomAttr('xmlns:atom', 'http://www.w3.org/2005/Atom')); + + $this->channelElement = $this->getDocument()->createElement('channel'); + $this->helper + ->buildNode($this->channelElement, 'generator', 'PicoFeed (https://github.com/fguillot/picoFeed)') + ->buildTitle($this->channelElement, $this->feedTitle) + ->buildNode($this->channelElement, 'description', $this->feedTitle) + ->buildDate($this->channelElement, $this->feedDate) + ->buildAuthor($this->channelElement, 'webMaster', $this->authorName, $this->authorEmail) + ->buildLink($this->channelElement, $this->siteUrl) + ; + + $link = $this->getDocument()->createElement('atom:link'); + $link->setAttribute('href', $this->feedUrl); + $link->setAttribute('rel', 'self'); + $link->setAttribute('type', 'application/rss+xml'); + $this->channelElement->appendChild($link); + + foreach ($this->items as $item) { + $this->channelElement->appendChild($item->build()); + } + + $this->rssElement->appendChild($this->channelElement); + $this->getDocument()->appendChild($this->rssElement); + + if ($filename !== '') { + $this->getDocument()->save($filename); + } + + return $this->getDocument()->saveXML(); + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/Rss20Helper.php b/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/Rss20Helper.php new file mode 100644 index 0000000..c99688a --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/Rss20Helper.php @@ -0,0 +1,115 @@ +document = $document; + } + + /** + * Build node + * + * @access public + * @param DOMElement $element + * @param string $tag + * @param string $value + * @return AtomHelper + */ + public function buildNode(DOMElement $element, $tag, $value) + { + $node = $this->document->createElement($tag); + $node->appendChild($this->document->createTextNode($value)); + $element->appendChild($node); + return $this; + } + + /** + * Build title + * + * @access public + * @param DOMElement $element + * @param string $title + * @return AtomHelper + */ + public function buildTitle(DOMElement $element, $title) + { + return $this->buildNode($element, 'title', $title); + } + + /** + * Build date element + * + * @access public + * @param DOMElement $element + * @param DateTime $date + * @param string $type + * @return AtomHelper + */ + public function buildDate(DOMElement $element, DateTime $date, $type = 'pubDate') + { + return $this->buildNode($element, $type, $date->format(DateTime::RSS)); + } + + /** + * Build link element + * + * @access public + * @param DOMElement $element + * @param string $url + * @return AtomHelper + */ + public function buildLink(DOMElement $element, $url) + { + return $this->buildNode($element, 'link', $url); + } + + /** + * Build author element + * + * @access public + * @param DOMElement $element + * @param string $tag + * @param string $authorName + * @param string $authorEmail + * @return AtomHelper + */ + public function buildAuthor(DOMElement $element, $tag, $authorName, $authorEmail) + { + if (!empty($authorName)) { + $value = ''; + + if (!empty($authorEmail)) { + $value .= $authorEmail.' ('.$authorName.')'; + } else { + $value = $authorName; + } + + $this->buildNode($element, $tag, $value); + } + + return $this; + } +} diff --git a/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/Rss20ItemBuilder.php b/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/Rss20ItemBuilder.php new file mode 100644 index 0000000..125dc6a --- /dev/null +++ b/vendor/fguillot/picofeed/lib/PicoFeed/Syndication/Rss20ItemBuilder.php @@ -0,0 +1,67 @@ +itemElement = $this->feedBuilder->getDocument()->createElement('item'); + $this->helper = new Rss20Helper($this->feedBuilder->getDocument()); + + if (!empty($this->itemId)) { + $guid = $this->feedBuilder->getDocument()->createElement('guid'); + $guid->setAttribute('isPermaLink', 'false'); + $guid->appendChild($this->feedBuilder->getDocument()->createTextNode($this->itemId)); + $this->itemElement->appendChild($guid); + } else { + $guid = $this->feedBuilder->getDocument()->createElement('guid'); + $guid->setAttribute('isPermaLink', 'true'); + $guid->appendChild($this->feedBuilder->getDocument()->createTextNode($this->itemUrl)); + $this->itemElement->appendChild($guid); + } + + $this->helper + ->buildTitle($this->itemElement, $this->itemTitle) + ->buildLink($this->itemElement, $this->itemUrl) + ->buildDate($this->itemElement, $this->itemPublishedDate) + ->buildAuthor($this->itemElement, 'author', $this->authorName, $this->authorEmail) + ; + + if (!empty($this->itemSummary)) { + $this->helper->buildNode($this->itemElement, 'description', $this->itemSummary); + } + + if (!empty($this->itemContent)) { + $node = $this->feedBuilder->getDocument()->createElement('content:encoded'); + $node->appendChild($this->feedBuilder->getDocument()->createCDATASection($this->itemContent)); + $this->itemElement->appendChild($node); + } + + return $this->itemElement; + } +} diff --git a/vendor/fguillot/picofeed/picofeed b/vendor/fguillot/picofeed/picofeed new file mode 100755 index 0000000..b1a8d80 --- /dev/null +++ b/vendor/fguillot/picofeed/picofeed @@ -0,0 +1,125 @@ +#!/usr/bin/env php +discover($url); + + $parser = $reader->getParser( + $resource->getUrl(), + $resource->getContent(), + $resource->getEncoding() + ); + + if ($disable_filtering) { + $parser->disableContentFiltering(); + } + + return $parser->execute(); + } + catch (PicoFeedException $e) { + echo 'Exception thrown ===> "'.$e->getMessage().'"'.PHP_EOL; + return false; + } +} + +function get_item($feed, $item_id) +{ + foreach ($feed->items as $item) { + if ($item->getId() === $item_id) { + echo $item; + echo "============= CONTENT ================\n"; + echo $item->getContent(); + echo "\n============= CONTENT ================\n"; + break; + } + } +} + +function dump_feed($url) +{ + $feed = get_feed($url); + echo $feed; +} + +function debug_feed($url) +{ + get_feed($url); + print_r(Logger::getMessages()); +} + +function dump_item($url, $item_id) +{ + $feed = get_feed($url); + + if ($feed !== false) { + get_item($feed, $item_id); + } +} + +function nofilter_item($url, $item_id) +{ + $feed = get_feed($url, true); + + if ($feed !== false) { + get_item($feed, $item_id); + } +} + +function grabber($url) +{ + $grabber = new Scraper(new Config); + $grabber->setUrl($url); + $grabber->execute(); + + print_r(Logger::getMessages()); + echo "============= CONTENT ================\n"; + echo $grabber->getRelevantContent().PHP_EOL; + echo "============= FILTERED ================\n"; + echo $grabber->getFilteredContent().PHP_EOL; +} + +// Parse command line arguments +if ($argc === 4) { + switch ($argv[1]) { + case 'item': + dump_item($argv[2], $argv[3]); + die; + case 'nofilter': + nofilter_item($argv[2], $argv[3]); + die; + } +} +else if ($argc === 3) { + switch ($argv[1]) { + case 'feed': + dump_feed($argv[2]); + die; + case 'debug': + debug_feed($argv[2]); + die; + case 'grabber': + grabber($argv[2]); + die; + } +} + +printf("Usage:\n"); +printf("%s feed \n", $argv[0]); +printf("%s debug \n", $argv[0]); +printf("%s item \n", $argv[0]); +printf("%s nofilter \n", $argv[0]); +printf("%s grabber \n", $argv[0]); diff --git a/vendor/nikic/fast-route/.travis.yml b/vendor/nikic/fast-route/.travis.yml new file mode 100644 index 0000000..e16ed63 --- /dev/null +++ b/vendor/nikic/fast-route/.travis.yml @@ -0,0 +1,12 @@ +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +matrix: + allow_failures: + - php: 7.0 diff --git a/vendor/nikic/fast-route/LICENSE b/vendor/nikic/fast-route/LICENSE new file mode 100644 index 0000000..478e764 --- /dev/null +++ b/vendor/nikic/fast-route/LICENSE @@ -0,0 +1,31 @@ +Copyright (c) 2013 by Nikita Popov. + +Some rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/nikic/fast-route/README.md b/vendor/nikic/fast-route/README.md new file mode 100644 index 0000000..ede20c5 --- /dev/null +++ b/vendor/nikic/fast-route/README.md @@ -0,0 +1,209 @@ +FastRoute - Fast request router for PHP +======================================= + +This library provides a fast implementation of a regular expression based router. [Blog post explaining how the +implementation works and why it is fast.][blog_post] + +Usage +----- + +Here's a basic usage example: + +```php +addRoute('GET', '/user/{id:\d+}', 'handler1'); + $r->addRoute('GET', '/user/{id:\d+}/{name}', 'handler2'); + // Or alternatively + $r->addRoute('GET', '/user/{id:\d+}[/{name}]', 'common_handler'); +}); + +$routeInfo = $dispatcher->dispatch($httpMethod, $uri); +switch ($routeInfo[0]) { + case FastRoute\Dispatcher::NOT_FOUND: + // ... 404 Not Found + break; + case FastRoute\Dispatcher::METHOD_NOT_ALLOWED: + $allowedMethods = $routeInfo[1]; + // ... 405 Method Not Allowed + break; + case FastRoute\Dispatcher::FOUND: + $handler = $routeInfo[1]; + $vars = $routeInfo[2]; + // ... call $handler with $vars + break; +} +``` + +### Defining routes + +The routes are defined by calling the `FastRoute\simpleDispatcher` function, which accepts +a callable taking a `FastRoute\RouteCollector` instance. The routes are added by calling +`addRoute()` on the collector instance. + +This method accepts the HTTP method the route must match, the route pattern and an associated +handler. The handler does not necessarily have to be a callback (it could also be a controller +class name or any other kind of data you wish to associate with the route). + +By default a route pattern syntax is used where `{foo}` specified a placeholder with name `foo` +and matching the string `[^/]+`. To adjust the pattern the placeholder matches, you can specify +a custom pattern by writing `{bar:[0-9]+}`. + +Furthermore parts of the route enclosed in `[...]` are considered optional, so that `/foo[bar]` +will match both `/foo` and `/foobar`. Optional parts are only supported in a trailing position, +not in the middle of a route. + +A custom pattern for a route placeholder must not use capturing groups. For example `{lang:(en|de)}` +is not a valid placeholder, because `()` is a capturing group. Instead you can use either +`{lang:en|de}` or `{lang:(?:en|de)}`. + +The reason `simpleDispatcher` accepts a callback for defining the routes is to allow seamless +caching. By using `cachedDispatcher` instead of `simpleDispatcher` you can cache the generated +routing data and construct the dispatcher from the cached information: + +```php +addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0'); + $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1'); + $r->addRoute('GET', '/user/{name}', 'handler2'); +}, [ + 'cacheFile' => __DIR__ . '/route.cache', /* required */ + 'cacheDisabled' => IS_DEBUG_ENABLED, /* optional, enabled by default */ +]); +``` + +The second parameter to the function is an options array, which can be used to specify the cache +file location, among other things. + +### Dispatching a URI + +A URI is dispatched by calling the `dispatch()` method of the created dispatcher. This method +accepts the HTTP method and a URI. Getting those two bits of information (and normalizing them +appropriately) is your job - this library is not bound to the PHP web SAPIs. + +The `dispatch()` method returns an array whose first element contains a status code. It is one +of `Dispatcher::NOT_FOUND`, `Dispatcher::METHOD_NOT_ALLOWED` and `Dispatcher::FOUND`. For the +method not allowed status the second array element contains a list of HTTP methods allowed for +the supplied URI. For example: + + [FastRoute\Dispatcher::METHOD_NOT_ALLOWED, ['GET', 'POST']] + +> **NOTE:** The HTTP specification requires that a `405 Method Not Allowed` response include the +`Allow:` header to detail available methods for the requested resource. Applications using FastRoute +should use the second array element to add this header when relaying a 405 response. + +For the found status the second array element is the handler that was associated with the route +and the third array element is a dictionary of placeholder names to their values. For example: + + /* Routing against GET /user/nikic/42 */ + + [FastRoute\Dispatcher::FOUND, 'handler0', ['name' => 'nikic', 'id' => '42']] + +### Overriding the route parser and dispatcher + +The routing process makes use of three components: A route parser, a data generator and a +dispatcher. The three components adhere to the following interfaces: + +```php + 'FastRoute\\RouteParser\\Std', + 'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased', + 'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased', +]); +``` + +The above options array corresponds to the defaults. By replacing `GroupCountBased` by +`GroupPosBased` you could switch to a different dispatching strategy. + +### A Note on HEAD Requests + +The HTTP spec requires servers to [support both GET and HEAD methods][2616-511]: + +> The methods GET and HEAD MUST be supported by all general-purpose servers + +To avoid forcing users to manually register HEAD routes for each resource we fallback to matching an +available GET route for a given resource. The PHP web SAPI transparently removes the entity body +from HEAD responses so this behavior has no effect on the vast majority of users. + +However, implementors using FastRoute outside the web SAPI environment (e.g. a custom server) MUST +NOT send entity bodies generated in response to HEAD requests. If you are a non-SAPI user this is +*your responsibility*; FastRoute has no purview to prevent you from breaking HTTP in such cases. + +Finally, note that applications MAY always specify their own HEAD method route for a given +resource to bypass this behavior entirely. + +### Credits + +This library is based on a router that [Levi Morrison][levi] implemented for the Aerys server. + +A large number of tests, as well as HTTP compliance considerations, were provided by [Daniel Lowrey][rdlowrey]. + + +[2616-511]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.1 "RFC 2616 Section 5.1.1" +[blog_post]: http://nikic.github.io/2014/02/18/Fast-request-routing-using-regular-expressions.html +[levi]: https://github.com/morrisonlevi +[rdlowrey]: https://github.com/rdlowrey diff --git a/vendor/nikic/fast-route/composer.json b/vendor/nikic/fast-route/composer.json new file mode 100644 index 0000000..62aad22 --- /dev/null +++ b/vendor/nikic/fast-route/composer.json @@ -0,0 +1,21 @@ +{ + "name": "nikic/fast-route", + "description": "Fast request router for PHP", + "keywords": ["routing", "router"], + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "require": { + "php": ">=5.4.0" + }, + "autoload": { + "psr-4": { + "FastRoute\\": "src/" + }, + "files": ["src/functions.php"] + } +} diff --git a/vendor/nikic/fast-route/phpunit.xml b/vendor/nikic/fast-route/phpunit.xml new file mode 100644 index 0000000..3c807b6 --- /dev/null +++ b/vendor/nikic/fast-route/phpunit.xml @@ -0,0 +1,24 @@ + + + + + + ./test/ + + + + + + ./src/ + + + diff --git a/vendor/nikic/fast-route/src/BadRouteException.php b/vendor/nikic/fast-route/src/BadRouteException.php new file mode 100644 index 0000000..7e38479 --- /dev/null +++ b/vendor/nikic/fast-route/src/BadRouteException.php @@ -0,0 +1,6 @@ + $route) { + $suffixLen++; + $suffix .= "\t"; + + $regexes[] = '(?:' . $regex . '/(\t{' . $suffixLen . '})\t{' . ($count - $suffixLen) . '})'; + $routeMap[$suffix] = [$route->handler, $route->variables]; + } + + $regex = '~^(?|' . implode('|', $regexes) . ')$~'; + return ['regex' => $regex, 'suffix' => '/' . $suffix, 'routeMap' => $routeMap]; + } +} diff --git a/vendor/nikic/fast-route/src/DataGenerator/GroupCountBased.php b/vendor/nikic/fast-route/src/DataGenerator/GroupCountBased.php new file mode 100644 index 0000000..d51807f --- /dev/null +++ b/vendor/nikic/fast-route/src/DataGenerator/GroupCountBased.php @@ -0,0 +1,28 @@ + $route) { + $numVariables = count($route->variables); + $numGroups = max($numGroups, $numVariables); + + $regexes[] = $regex . str_repeat('()', $numGroups - $numVariables); + $routeMap[$numGroups + 1] = [$route->handler, $route->variables]; + + ++$numGroups; + } + + $regex = '~^(?|' . implode('|', $regexes) . ')$~'; + return ['regex' => $regex, 'routeMap' => $routeMap]; + } +} + diff --git a/vendor/nikic/fast-route/src/DataGenerator/GroupPosBased.php b/vendor/nikic/fast-route/src/DataGenerator/GroupPosBased.php new file mode 100644 index 0000000..4152f7a --- /dev/null +++ b/vendor/nikic/fast-route/src/DataGenerator/GroupPosBased.php @@ -0,0 +1,25 @@ + $route) { + $regexes[] = $regex; + $routeMap[$offset] = [$route->handler, $route->variables]; + + $offset += count($route->variables); + } + + $regex = '~^(?:' . implode('|', $regexes) . ')$~'; + return ['regex' => $regex, 'routeMap' => $routeMap]; + } +} + diff --git a/vendor/nikic/fast-route/src/DataGenerator/MarkBased.php b/vendor/nikic/fast-route/src/DataGenerator/MarkBased.php new file mode 100644 index 0000000..61359f5 --- /dev/null +++ b/vendor/nikic/fast-route/src/DataGenerator/MarkBased.php @@ -0,0 +1,25 @@ + $route) { + $regexes[] = $regex . '(*MARK:' . $markName . ')'; + $routeMap[$markName] = [$route->handler, $route->variables]; + + ++$markName; + } + + $regex = '~^(?|' . implode('|', $regexes) . ')$~'; + return ['regex' => $regex, 'routeMap' => $routeMap]; + } +} + diff --git a/vendor/nikic/fast-route/src/DataGenerator/RegexBasedAbstract.php b/vendor/nikic/fast-route/src/DataGenerator/RegexBasedAbstract.php new file mode 100644 index 0000000..7ba75a6 --- /dev/null +++ b/vendor/nikic/fast-route/src/DataGenerator/RegexBasedAbstract.php @@ -0,0 +1,144 @@ +isStaticRoute($routeData)) { + $this->addStaticRoute($httpMethod, $routeData, $handler); + } else { + $this->addVariableRoute($httpMethod, $routeData, $handler); + } + } + + public function getData() { + if (empty($this->methodToRegexToRoutesMap)) { + return [$this->staticRoutes, []]; + } + + return [$this->staticRoutes, $this->generateVariableRouteData()]; + } + + private function generateVariableRouteData() { + $data = []; + foreach ($this->methodToRegexToRoutesMap as $method => $regexToRoutesMap) { + $chunkSize = $this->computeChunkSize(count($regexToRoutesMap)); + $chunks = array_chunk($regexToRoutesMap, $chunkSize, true); + $data[$method] = array_map([$this, 'processChunk'], $chunks); + } + return $data; + } + + private function computeChunkSize($count) { + $numParts = max(1, round($count / $this->getApproxChunkSize())); + return ceil($count / $numParts); + } + + private function isStaticRoute($routeData) { + return count($routeData) == 1 && is_string($routeData[0]); + } + + private function addStaticRoute($httpMethod, $routeData, $handler) { + $routeStr = $routeData[0]; + + if (isset($this->staticRoutes[$httpMethod][$routeStr])) { + throw new BadRouteException(sprintf( + 'Cannot register two routes matching "%s" for method "%s"', + $routeStr, $httpMethod + )); + } + + if (isset($this->methodToRegexToRoutesMap[$httpMethod])) { + foreach ($this->methodToRegexToRoutesMap[$httpMethod] as $route) { + if ($route->matches($routeStr)) { + throw new BadRouteException(sprintf( + 'Static route "%s" is shadowed by previously defined variable route "%s" for method "%s"', + $routeStr, $route->regex, $httpMethod + )); + } + } + } + + $this->staticRoutes[$httpMethod][$routeStr] = $handler; + } + + private function addVariableRoute($httpMethod, $routeData, $handler) { + list($regex, $variables) = $this->buildRegexForRoute($routeData); + + if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) { + throw new BadRouteException(sprintf( + 'Cannot register two routes matching "%s" for method "%s"', + $regex, $httpMethod + )); + } + + $this->methodToRegexToRoutesMap[$httpMethod][$regex] = new Route( + $httpMethod, $handler, $regex, $variables + ); + } + + private function buildRegexForRoute($routeData) { + $regex = ''; + $variables = []; + foreach ($routeData as $part) { + if (is_string($part)) { + $regex .= preg_quote($part, '~'); + continue; + } + + list($varName, $regexPart) = $part; + + if (isset($variables[$varName])) { + throw new BadRouteException(sprintf( + 'Cannot use the same placeholder "%s" twice', $varName + )); + } + + if ($this->regexHasCapturingGroups($regexPart)) { + throw new BadRouteException(sprintf( + 'Regex "%s" for parameter "%s" contains a capturing group', + $regexPart, $varName + )); + } + + $variables[$varName] = $varName; + $regex .= '(' . $regexPart . ')'; + } + + return [$regex, $variables]; + } + + private function regexHasCapturingGroups($regex) { + if (false === strpos($regex, '(')) { + // Needs to have at least a ( to contain a capturing group + return false; + } + + // Semi-accurate detection for capturing groups + return preg_match( + '~ + (?: + \(\?\( + | \[ [^\]\\\\]* (?: \\\\ . [^\]\\\\]* )* \] + | \\\\ . + ) (*SKIP)(*FAIL) | + \( + (?! + \? (?! <(?![!=]) | P< | \' ) + | \* + ) + ~x', + $regex + ); + } +} diff --git a/vendor/nikic/fast-route/src/Dispatcher.php b/vendor/nikic/fast-route/src/Dispatcher.php new file mode 100644 index 0000000..ea98009 --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher.php @@ -0,0 +1,25 @@ + 'value', ...]] + * + * @param string $httpMethod + * @param string $uri + * + * @return array + */ + public function dispatch($httpMethod, $uri); +} diff --git a/vendor/nikic/fast-route/src/Dispatcher/CharCountBased.php b/vendor/nikic/fast-route/src/Dispatcher/CharCountBased.php new file mode 100644 index 0000000..22ba240 --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher/CharCountBased.php @@ -0,0 +1,28 @@ +staticRouteMap, $this->variableRouteData) = $data; + } + + protected function dispatchVariableRoute($routeData, $uri) { + foreach ($routeData as $data) { + if (!preg_match($data['regex'], $uri . $data['suffix'], $matches)) { + continue; + } + + list($handler, $varNames) = $data['routeMap'][end($matches)]; + + $vars = []; + $i = 0; + foreach ($varNames as $varName) { + $vars[$varName] = $matches[++$i]; + } + return [self::FOUND, $handler, $vars]; + } + + return [self::NOT_FOUND]; + } +} diff --git a/vendor/nikic/fast-route/src/Dispatcher/GroupCountBased.php b/vendor/nikic/fast-route/src/Dispatcher/GroupCountBased.php new file mode 100644 index 0000000..0abd322 --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher/GroupCountBased.php @@ -0,0 +1,28 @@ +staticRouteMap, $this->variableRouteData) = $data; + } + + protected function dispatchVariableRoute($routeData, $uri) { + foreach ($routeData as $data) { + if (!preg_match($data['regex'], $uri, $matches)) { + continue; + } + + list($handler, $varNames) = $data['routeMap'][count($matches)]; + + $vars = []; + $i = 0; + foreach ($varNames as $varName) { + $vars[$varName] = $matches[++$i]; + } + return [self::FOUND, $handler, $vars]; + } + + return [self::NOT_FOUND]; + } +} diff --git a/vendor/nikic/fast-route/src/Dispatcher/GroupPosBased.php b/vendor/nikic/fast-route/src/Dispatcher/GroupPosBased.php new file mode 100644 index 0000000..32227d4 --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher/GroupPosBased.php @@ -0,0 +1,30 @@ +staticRouteMap, $this->variableRouteData) = $data; + } + + protected function dispatchVariableRoute($routeData, $uri) { + foreach ($routeData as $data) { + if (!preg_match($data['regex'], $uri, $matches)) { + continue; + } + + // find first non-empty match + for ($i = 1; '' === $matches[$i]; ++$i); + + list($handler, $varNames) = $data['routeMap'][$i]; + + $vars = []; + foreach ($varNames as $varName) { + $vars[$varName] = $matches[$i++]; + } + return [self::FOUND, $handler, $vars]; + } + + return [self::NOT_FOUND]; + } +} diff --git a/vendor/nikic/fast-route/src/Dispatcher/MarkBased.php b/vendor/nikic/fast-route/src/Dispatcher/MarkBased.php new file mode 100644 index 0000000..fefa711 --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher/MarkBased.php @@ -0,0 +1,28 @@ +staticRouteMap, $this->variableRouteData) = $data; + } + + protected function dispatchVariableRoute($routeData, $uri) { + foreach ($routeData as $data) { + if (!preg_match($data['regex'], $uri, $matches)) { + continue; + } + + list($handler, $varNames) = $data['routeMap'][$matches['MARK']]; + + $vars = []; + $i = 0; + foreach ($varNames as $varName) { + $vars[$varName] = $matches[++$i]; + } + return [self::FOUND, $handler, $vars]; + } + + return [self::NOT_FOUND]; + } +} diff --git a/vendor/nikic/fast-route/src/Dispatcher/RegexBasedAbstract.php b/vendor/nikic/fast-route/src/Dispatcher/RegexBasedAbstract.php new file mode 100644 index 0000000..20b8aae --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher/RegexBasedAbstract.php @@ -0,0 +1,62 @@ +staticRouteMap[$httpMethod][$uri])) { + $handler = $this->staticRouteMap[$httpMethod][$uri]; + return [self::FOUND, $handler, []]; + } else if ($httpMethod === 'HEAD' && isset($this->staticRouteMap['GET'][$uri])) { + $handler = $this->staticRouteMap['GET'][$uri]; + return [self::FOUND, $handler, []]; + } + + $varRouteData = $this->variableRouteData; + if (isset($varRouteData[$httpMethod])) { + $result = $this->dispatchVariableRoute($varRouteData[$httpMethod], $uri); + if ($result[0] === self::FOUND) { + return $result; + } + } else if ($httpMethod === 'HEAD' && isset($varRouteData['GET'])) { + $result = $this->dispatchVariableRoute($varRouteData['GET'], $uri); + if ($result[0] === self::FOUND) { + return $result; + } + } + + // Find allowed methods for this URI by matching against all other HTTP methods as well + $allowedMethods = []; + + foreach ($this->staticRouteMap as $method => $uriMap) { + if ($method !== $httpMethod && isset($uriMap[$uri])) { + $allowedMethods[] = $method; + } + } + + foreach ($varRouteData as $method => $routeData) { + if ($method === $httpMethod) { + continue; + } + + $result = $this->dispatchVariableRoute($routeData, $uri); + if ($result[0] === self::FOUND) { + $allowedMethods[] = $method; + } + } + + // If there are no allowed methods the route simply does not exist + if ($allowedMethods) { + return [self::METHOD_NOT_ALLOWED, $allowedMethods]; + } else { + return [self::NOT_FOUND]; + } + } +} diff --git a/vendor/nikic/fast-route/src/Route.php b/vendor/nikic/fast-route/src/Route.php new file mode 100644 index 0000000..d71ded1 --- /dev/null +++ b/vendor/nikic/fast-route/src/Route.php @@ -0,0 +1,38 @@ +httpMethod = $httpMethod; + $this->handler = $handler; + $this->regex = $regex; + $this->variables = $variables; + } + + /** + * Tests whether this route matches the given string. + * + * @param string $str + * + * @return bool + */ + public function matches($str) { + $regex = '~^' . $this->regex . '$~'; + return (bool) preg_match($regex, $str); + } +} + diff --git a/vendor/nikic/fast-route/src/RouteCollector.php b/vendor/nikic/fast-route/src/RouteCollector.php new file mode 100644 index 0000000..4386bbf --- /dev/null +++ b/vendor/nikic/fast-route/src/RouteCollector.php @@ -0,0 +1,46 @@ +routeParser = $routeParser; + $this->dataGenerator = $dataGenerator; + } + + /** + * Adds a route to the collection. + * + * The syntax used in the $route string depends on the used route parser. + * + * @param string|string[] $httpMethod + * @param string $route + * @param mixed $handler + */ + public function addRoute($httpMethod, $route, $handler) { + $routeDatas = $this->routeParser->parse($route); + foreach ((array) $httpMethod as $method) { + foreach ($routeDatas as $routeData) { + $this->dataGenerator->addRoute($method, $routeData, $handler); + } + } + } + + /** + * Returns the collected route data, as provided by the data generator. + * + * @return array + */ + public function getData() { + return $this->dataGenerator->getData(); + } +} diff --git a/vendor/nikic/fast-route/src/RouteParser.php b/vendor/nikic/fast-route/src/RouteParser.php new file mode 100644 index 0000000..c089c31 --- /dev/null +++ b/vendor/nikic/fast-route/src/RouteParser.php @@ -0,0 +1,36 @@ +parsePlaceholders($currentRoute); + } + return $routeDatas; + } + + /** + * Parses a route string that does not contain optional segments. + */ + private function parsePlaceholders($route) { + if (!preg_match_all( + '~' . self::VARIABLE_REGEX . '~x', $route, $matches, + PREG_OFFSET_CAPTURE | PREG_SET_ORDER + )) { + return [$route]; + } + + $offset = 0; + $routeData = []; + foreach ($matches as $set) { + if ($set[0][1] > $offset) { + $routeData[] = substr($route, $offset, $set[0][1] - $offset); + } + $routeData[] = [ + $set[1][0], + isset($set[2]) ? trim($set[2][0]) : self::DEFAULT_DISPATCH_REGEX + ]; + $offset = $set[0][1] + strlen($set[0][0]); + } + + if ($offset != strlen($route)) { + $routeData[] = substr($route, $offset); + } + + return $routeData; + } +} diff --git a/vendor/nikic/fast-route/src/bootstrap.php b/vendor/nikic/fast-route/src/bootstrap.php new file mode 100644 index 0000000..add216c --- /dev/null +++ b/vendor/nikic/fast-route/src/bootstrap.php @@ -0,0 +1,12 @@ + 'FastRoute\\RouteParser\\Std', + 'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased', + 'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased', + 'routeCollector' => 'FastRoute\\RouteCollector', + ]; + + /** @var RouteCollector $routeCollector */ + $routeCollector = new $options['routeCollector']( + new $options['routeParser'], new $options['dataGenerator'] + ); + $routeDefinitionCallback($routeCollector); + + return new $options['dispatcher']($routeCollector->getData()); + } + + /** + * @param callable $routeDefinitionCallback + * @param array $options + * + * @return Dispatcher + */ + function cachedDispatcher(callable $routeDefinitionCallback, array $options = []) { + $options += [ + 'routeParser' => 'FastRoute\\RouteParser\\Std', + 'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased', + 'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased', + 'routeCollector' => 'FastRoute\\RouteCollector', + 'cacheDisabled' => false, + ]; + + if (!isset($options['cacheFile'])) { + throw new \LogicException('Must specify "cacheFile" option'); + } + + if (!$options['cacheDisabled'] && file_exists($options['cacheFile'])) { + $dispatchData = require $options['cacheFile']; + if (!is_array($dispatchData)) { + throw new \RuntimeException('Invalid cache file "' . $options['cacheFile'] . '"'); + } + return new $options['dispatcher']($dispatchData); + } + + $routeCollector = new $options['routeCollector']( + new $options['routeParser'], new $options['dataGenerator'] + ); + $routeDefinitionCallback($routeCollector); + + /** @var RouteCollector $routeCollector */ + $dispatchData = $routeCollector->getData(); + file_put_contents( + $options['cacheFile'], + ' $this->getDataGeneratorClass(), + 'dispatcher' => $this->getDispatcherClass() + ]; + } + + /** + * @dataProvider provideFoundDispatchCases + */ + public function testFoundDispatches($method, $uri, $callback, $handler, $argDict) { + $dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions()); + $info = $dispatcher->dispatch($method, $uri); + $this->assertSame($dispatcher::FOUND, $info[0]); + $this->assertSame($handler, $info[1]); + $this->assertSame($argDict, $info[2]); + } + + /** + * @dataProvider provideNotFoundDispatchCases + */ + public function testNotFoundDispatches($method, $uri, $callback) { + $dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions()); + $routeInfo = $dispatcher->dispatch($method, $uri); + $this->assertFalse(isset($routeInfo[1]), + "NOT_FOUND result must only contain a single element in the returned info array" + ); + $this->assertSame($dispatcher::NOT_FOUND, $routeInfo[0]); + } + + /** + * @dataProvider provideMethodNotAllowedDispatchCases + */ + public function testMethodNotAllowedDispatches($method, $uri, $callback, $availableMethods) { + $dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions()); + $routeInfo = $dispatcher->dispatch($method, $uri); + $this->assertTrue(isset($routeInfo[1]), + "METHOD_NOT_ALLOWED result must return an array of allowed methods at index 1" + ); + + list($routedStatus, $methodArray) = $dispatcher->dispatch($method, $uri); + $this->assertSame($dispatcher::METHOD_NOT_ALLOWED, $routedStatus); + $this->assertSame($availableMethods, $methodArray); + } + + /** + * @expectedException \FastRoute\BadRouteException + * @expectedExceptionMessage Cannot use the same placeholder "test" twice + */ + public function testDuplicateVariableNameError() { + \FastRoute\simpleDispatcher(function(RouteCollector $r) { + $r->addRoute('GET', '/foo/{test}/{test:\d+}', 'handler0'); + }, $this->generateDispatcherOptions()); + } + + /** + * @expectedException \FastRoute\BadRouteException + * @expectedExceptionMessage Cannot register two routes matching "/user/([^/]+)" for method "GET" + */ + public function testDuplicateVariableRoute() { + \FastRoute\simpleDispatcher(function(RouteCollector $r) { + $r->addRoute('GET', '/user/{id}', 'handler0'); // oops, forgot \d+ restriction ;) + $r->addRoute('GET', '/user/{name}', 'handler1'); + }, $this->generateDispatcherOptions()); + } + + /** + * @expectedException \FastRoute\BadRouteException + * @expectedExceptionMessage Cannot register two routes matching "/user" for method "GET" + */ + public function testDuplicateStaticRoute() { + \FastRoute\simpleDispatcher(function(RouteCollector $r) { + $r->addRoute('GET', '/user', 'handler0'); + $r->addRoute('GET', '/user', 'handler1'); + }, $this->generateDispatcherOptions()); + } + + /** + * @expectedException \FastRoute\BadRouteException + * @expectedExceptionMessage Static route "/user/nikic" is shadowed by previously defined variable route "/user/([^/]+)" for method "GET" + */ + public function testShadowedStaticRoute() { + \FastRoute\simpleDispatcher(function(RouteCollector $r) { + $r->addRoute('GET', '/user/{name}', 'handler0'); + $r->addRoute('GET', '/user/nikic', 'handler1'); + }, $this->generateDispatcherOptions()); + } + + /** + * @expectedException \FastRoute\BadRouteException + * @expectedExceptionMessage Regex "(en|de)" for parameter "lang" contains a capturing group + */ + public function testCapturing() { + \FastRoute\simpleDispatcher(function(RouteCollector $r) { + $r->addRoute('GET', '/{lang:(en|de)}', 'handler0'); + }, $this->generateDispatcherOptions()); + } + + public function provideFoundDispatchCases() { + $cases = []; + + // 0 --------------------------------------------------------------------------------------> + + $callback = function(RouteCollector $r) { + $r->addRoute('GET', '/resource/123/456', 'handler0'); + }; + + $method = 'GET'; + $uri = '/resource/123/456'; + $handler = 'handler0'; + $argDict = []; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 1 --------------------------------------------------------------------------------------> + + $callback = function(RouteCollector $r) { + $r->addRoute('GET', '/handler0', 'handler0'); + $r->addRoute('GET', '/handler1', 'handler1'); + $r->addRoute('GET', '/handler2', 'handler2'); + }; + + $method = 'GET'; + $uri = '/handler2'; + $handler = 'handler2'; + $argDict = []; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 2 --------------------------------------------------------------------------------------> + + $callback = function(RouteCollector $r) { + $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0'); + $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1'); + $r->addRoute('GET', '/user/{name}', 'handler2'); + }; + + $method = 'GET'; + $uri = '/user/rdlowrey'; + $handler = 'handler2'; + $argDict = ['name' => 'rdlowrey']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 3 --------------------------------------------------------------------------------------> + + // reuse $callback from #2 + + $method = 'GET'; + $uri = '/user/12345'; + $handler = 'handler1'; + $argDict = ['id' => '12345']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 4 --------------------------------------------------------------------------------------> + + // reuse $callback from #3 + + $method = 'GET'; + $uri = '/user/NaN'; + $handler = 'handler2'; + $argDict = ['name' => 'NaN']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 5 --------------------------------------------------------------------------------------> + + // reuse $callback from #4 + + $method = 'GET'; + $uri = '/user/rdlowrey/12345'; + $handler = 'handler0'; + $argDict = ['name' => 'rdlowrey', 'id' => '12345']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 6 --------------------------------------------------------------------------------------> + + $callback = function(RouteCollector $r) { + $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler0'); + $r->addRoute('GET', '/user/12345/extension', 'handler1'); + $r->addRoute('GET', '/user/{id:[0-9]+}.{extension}', 'handler2'); + + }; + + $method = 'GET'; + $uri = '/user/12345.svg'; + $handler = 'handler2'; + $argDict = ['id' => '12345', 'extension' => 'svg']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 7 ----- Test GET method fallback on HEAD route miss ------------------------------------> + + $callback = function(RouteCollector $r) { + $r->addRoute('GET', '/user/{name}', 'handler0'); + $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler1'); + $r->addRoute('GET', '/static0', 'handler2'); + $r->addRoute('GET', '/static1', 'handler3'); + $r->addRoute('HEAD', '/static1', 'handler4'); + }; + + $method = 'HEAD'; + $uri = '/user/rdlowrey'; + $handler = 'handler0'; + $argDict = ['name' => 'rdlowrey']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 8 ----- Test GET method fallback on HEAD route miss ------------------------------------> + + // reuse $callback from #7 + + $method = 'HEAD'; + $uri = '/user/rdlowrey/1234'; + $handler = 'handler1'; + $argDict = ['name' => 'rdlowrey', 'id' => '1234']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 9 ----- Test GET method fallback on HEAD route miss ------------------------------------> + + // reuse $callback from #8 + + $method = 'HEAD'; + $uri = '/static0'; + $handler = 'handler2'; + $argDict = []; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 10 ---- Test existing HEAD route used if available (no fallback) -----------------------> + + // reuse $callback from #9 + + $method = 'HEAD'; + $uri = '/static1'; + $handler = 'handler4'; + $argDict = []; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 11 ---- More specified routes are not shadowed by less specific of another method ------> + + $callback = function(RouteCollector $r) { + $r->addRoute('GET', '/user/{name}', 'handler0'); + $r->addRoute('POST', '/user/{name:[a-z]+}', 'handler1'); + }; + + $method = 'POST'; + $uri = '/user/rdlowrey'; + $handler = 'handler1'; + $argDict = ['name' => 'rdlowrey']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 12 ---- Handler of more specific routes is used, if it occurs first --------------------> + + $callback = function(RouteCollector $r) { + $r->addRoute('GET', '/user/{name}', 'handler0'); + $r->addRoute('POST', '/user/{name:[a-z]+}', 'handler1'); + $r->addRoute('POST', '/user/{name}', 'handler2'); + }; + + $method = 'POST'; + $uri = '/user/rdlowrey'; + $handler = 'handler1'; + $argDict = ['name' => 'rdlowrey']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 13 ---- Route with constant suffix -----------------------------------------------------> + + $callback = function(RouteCollector $r) { + $r->addRoute('GET', '/user/{name}', 'handler0'); + $r->addRoute('GET', '/user/{name}/edit', 'handler1'); + }; + + $method = 'GET'; + $uri = '/user/rdlowrey/edit'; + $handler = 'handler1'; + $argDict = ['name' => 'rdlowrey']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 14 ---- Handle multiple methods with the same handler ----------------------------------> + + $callback = function(RouteCollector $r) { + $r->addRoute(['GET', 'POST'], '/user', 'handlerGetPost'); + $r->addRoute(['DELETE'], '/user', 'handlerDelete'); + $r->addRoute([], '/user', 'handlerNone'); + }; + + $argDict = []; + $cases[] = ['GET', '/user', $callback, 'handlerGetPost', $argDict]; + $cases[] = ['POST', '/user', $callback, 'handlerGetPost', $argDict]; + $cases[] = ['DELETE', '/user', $callback, 'handlerDelete', $argDict]; + + // 15 ---- + + $callback = function(RouteCollector $r) { + $r->addRoute('POST', '/user.json', 'handler0'); + $r->addRoute('GET', '/{entity}.json', 'handler1'); + }; + + $cases[] = ['GET', '/user.json', $callback, 'handler1', ['entity' => 'user']]; + + + // x --------------------------------------------------------------------------------------> + + return $cases; + } + + public function provideNotFoundDispatchCases() { + $cases = []; + + // 0 --------------------------------------------------------------------------------------> + + $callback = function(RouteCollector $r) { + $r->addRoute('GET', '/resource/123/456', 'handler0'); + }; + + $method = 'GET'; + $uri = '/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 1 --------------------------------------------------------------------------------------> + + // reuse callback from #0 + $method = 'POST'; + $uri = '/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 2 --------------------------------------------------------------------------------------> + + // reuse callback from #1 + $method = 'PUT'; + $uri = '/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 3 --------------------------------------------------------------------------------------> + + $callback = function(RouteCollector $r) { + $r->addRoute('GET', '/handler0', 'handler0'); + $r->addRoute('GET', '/handler1', 'handler1'); + $r->addRoute('GET', '/handler2', 'handler2'); + }; + + $method = 'GET'; + $uri = '/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 4 --------------------------------------------------------------------------------------> + + $callback = function(RouteCollector $r) { + $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0'); + $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1'); + $r->addRoute('GET', '/user/{name}', 'handler2'); + }; + + $method = 'GET'; + $uri = '/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 5 --------------------------------------------------------------------------------------> + + // reuse callback from #4 + $method = 'GET'; + $uri = '/user/rdlowrey/12345/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 6 --------------------------------------------------------------------------------------> + + // reuse callback from #5 + $method = 'HEAD'; + + $cases[] = array($method, $uri, $callback); + + // x --------------------------------------------------------------------------------------> + + return $cases; + } + + public function provideMethodNotAllowedDispatchCases() { + $cases = []; + + // 0 --------------------------------------------------------------------------------------> + + $callback = function(RouteCollector $r) { + $r->addRoute('GET', '/resource/123/456', 'handler0'); + }; + + $method = 'POST'; + $uri = '/resource/123/456'; + $allowedMethods = ['GET']; + + $cases[] = [$method, $uri, $callback, $allowedMethods]; + + // 1 --------------------------------------------------------------------------------------> + + $callback = function(RouteCollector $r) { + $r->addRoute('GET', '/resource/123/456', 'handler0'); + $r->addRoute('POST', '/resource/123/456', 'handler1'); + $r->addRoute('PUT', '/resource/123/456', 'handler2'); + }; + + $method = 'DELETE'; + $uri = '/resource/123/456'; + $allowedMethods = ['GET', 'POST', 'PUT']; + + $cases[] = [$method, $uri, $callback, $allowedMethods]; + + // 2 --------------------------------------------------------------------------------------> + + $callback = function(RouteCollector $r) { + $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0'); + $r->addRoute('POST', '/user/{name}/{id:[0-9]+}', 'handler1'); + $r->addRoute('PUT', '/user/{name}/{id:[0-9]+}', 'handler2'); + $r->addRoute('PATCH', '/user/{name}/{id:[0-9]+}', 'handler3'); + }; + + $method = 'DELETE'; + $uri = '/user/rdlowrey/42'; + $allowedMethods = ['GET', 'POST', 'PUT', 'PATCH']; + + $cases[] = [$method, $uri, $callback, $allowedMethods]; + + // 3 --------------------------------------------------------------------------------------> + + $callback = function(RouteCollector $r) { + $r->addRoute('POST', '/user/{name}', 'handler1'); + $r->addRoute('PUT', '/user/{name:[a-z]+}', 'handler2'); + $r->addRoute('PATCH', '/user/{name:[a-z]+}', 'handler3'); + }; + + $method = 'GET'; + $uri = '/user/rdlowrey'; + $allowedMethods = ['POST', 'PUT', 'PATCH']; + + $cases[] = [$method, $uri, $callback, $allowedMethods]; + + // 4 --------------------------------------------------------------------------------------> + + $callback = function(RouteCollector $r) { + $r->addRoute(['GET', 'POST'], '/user', 'handlerGetPost'); + $r->addRoute(['DELETE'], '/user', 'handlerDelete'); + $r->addRoute([], '/user', 'handlerNone'); + }; + + $cases[] = ['PUT', '/user', $callback, ['GET', 'POST', 'DELETE']]; + + // 5 + + $callback = function(RouteCollector $r) { + $r->addRoute('POST', '/user.json', 'handler0'); + $r->addRoute('GET', '/{entity}.json', 'handler1'); + }; + + $cases[] = ['PUT', '/user.json', $callback, ['POST', 'GET']]; + + // x --------------------------------------------------------------------------------------> + + return $cases; + } + +} diff --git a/vendor/nikic/fast-route/test/Dispatcher/GroupCountBasedTest.php b/vendor/nikic/fast-route/test/Dispatcher/GroupCountBasedTest.php new file mode 100644 index 0000000..74820fc --- /dev/null +++ b/vendor/nikic/fast-route/test/Dispatcher/GroupCountBasedTest.php @@ -0,0 +1,13 @@ +markTestSkipped('PHP 5.6 required for MARK support'); + } + } + + protected function getDispatcherClass() { + return 'FastRoute\\Dispatcher\\MarkBased'; + } + + protected function getDataGeneratorClass() { + return 'FastRoute\\DataGenerator\\MarkBased'; + } +} diff --git a/vendor/nikic/fast-route/test/RouteParser/StdTest.php b/vendor/nikic/fast-route/test/RouteParser/StdTest.php new file mode 100644 index 0000000..9a25e1f --- /dev/null +++ b/vendor/nikic/fast-route/test/RouteParser/StdTest.php @@ -0,0 +1,114 @@ +parse($routeString); + $this->assertSame($expectedRouteDatas, $routeDatas); + } + + /** @dataProvider provideTestParseError */ + public function testParseError($routeString, $expectedExceptionMessage) { + $parser = new Std(); + $this->setExpectedException('FastRoute\\BadRouteException', $expectedExceptionMessage); + $parser->parse($routeString); + } + + public function provideTestParse() { + return [ + [ + '/test', + [ + ['/test'], + ] + ], + [ + '/test/{param}', + [ + ['/test/', ['param', '[^/]+']], + ] + ], + [ + '/te{ param }st', + [ + ['/te', ['param', '[^/]+'], 'st'] + ] + ], + [ + '/test/{param1}/test2/{param2}', + [ + ['/test/', ['param1', '[^/]+'], '/test2/', ['param2', '[^/]+']] + ] + ], + [ + '/test/{param:\d+}', + [ + ['/test/', ['param', '\d+']] + ] + ], + [ + '/test/{ param : \d{1,9} }', + [ + ['/test/', ['param', '\d{1,9}']] + ] + ], + [ + '/test[opt]', + [ + ['/test'], + ['/testopt'], + ] + ], + [ + '/test[/{param}]', + [ + ['/test'], + ['/test/', ['param', '[^/]+']], + ] + ], + [ + '/{param}[opt]', + [ + ['/', ['param', '[^/]+']], + ['/', ['param', '[^/]+'], 'opt'] + ] + ], + [ + '/test[/{name}[/{id:[0-9]+}]]', + [ + ['/test'], + ['/test/', ['name', '[^/]+']], + ['/test/', ['name', '[^/]+'], '/', ['id', '[0-9]+']], + ] + ], + ]; + } + + public function provideTestParseError() { + return [ + [ + '/test[opt', + "Number of opening '[' and closing ']' does not match" + ], + [ + '/test[opt[opt2]', + "Number of opening '[' and closing ']' does not match" + ], + [ + '/testopt]', + "Number of opening '[' and closing ']' does not match" + ], + [ + '/test[]', + "Empty optional part" + ], + [ + '/test[[opt]]', + "Empty optional part" + ], + ]; + } +} diff --git a/vendor/nikic/fast-route/test/bootstrap.php b/vendor/nikic/fast-route/test/bootstrap.php new file mode 100644 index 0000000..27e6d4c --- /dev/null +++ b/vendor/nikic/fast-route/test/bootstrap.php @@ -0,0 +1,11 @@ +> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"`; fi + +script: + - cd ext/pimple + - if [ "$PIMPLE_EXT" == "yes" ]; then yes n | make test | tee output ; grep -E 'Tests failed +. +0' output; fi + - cd ../.. + - phpunit + +matrix: + exclude: + - php: hhvm + env: PIMPLE_EXT=yes diff --git a/vendor/pimple/pimple/CHANGELOG b/vendor/pimple/pimple/CHANGELOG new file mode 100644 index 0000000..cc67997 --- /dev/null +++ b/vendor/pimple/pimple/CHANGELOG @@ -0,0 +1,35 @@ +* 3.0.2 (2015-09-11) + + * refactored the C extension + * minor non-significant changes + +* 3.0.1 (2015-07-30) + + * simplified some code + * fixed a segfault in the C extension + +* 3.0.0 (2014-07-24) + + * removed the Pimple class alias (use Pimple\Container instead) + +* 2.1.1 (2014-07-24) + + * fixed compiler warnings for the C extension + * fixed code when dealing with circular references + +* 2.1.0 (2014-06-24) + + * moved the Pimple to Pimple\Container (with a BC layer -- Pimple is now a + deprecated alias which will be removed in Pimple 3.0) + * added Pimple\ServiceProviderInterface (and Pimple::register()) + +* 2.0.0 (2014-02-10) + + * changed extend to automatically re-assign the extended service and keep it as shared or factory + (to keep BC, extend still returns the extended service) + * changed services to be shared by default (use factory() for factory + services) + +* 1.0.0 + + * initial version diff --git a/vendor/pimple/pimple/LICENSE b/vendor/pimple/pimple/LICENSE new file mode 100644 index 0000000..d7949e2 --- /dev/null +++ b/vendor/pimple/pimple/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2009-2015 Fabien Potencier + +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: + +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/vendor/pimple/pimple/README.rst b/vendor/pimple/pimple/README.rst new file mode 100644 index 0000000..93fb35a --- /dev/null +++ b/vendor/pimple/pimple/README.rst @@ -0,0 +1,201 @@ +Pimple +====== + +.. caution:: + + This is the documentation for Pimple 3.x. If you are using Pimple 1.x, read + the `Pimple 1.x documentation`_. Reading the Pimple 1.x code is also a good + way to learn more about how to create a simple Dependency Injection + Container (recent versions of Pimple are more focused on performance). + +Pimple is a small Dependency Injection Container for PHP. + +Installation +------------ + +Before using Pimple in your project, add it to your ``composer.json`` file: + +.. code-block:: bash + + $ ./composer.phar require pimple/pimple ~3.0 + +Alternatively, Pimple is also available as a PHP C extension: + +.. code-block:: bash + + $ git clone https://github.com/silexphp/Pimple + $ cd Pimple/ext/pimple + $ phpize + $ ./configure + $ make + $ make install + +Usage +----- + +Creating a container is a matter of creating a ``Container`` instance: + +.. code-block:: php + + use Pimple\Container; + + $container = new Container(); + +As many other dependency injection containers, Pimple manages two different +kind of data: **services** and **parameters**. + +Defining Services +~~~~~~~~~~~~~~~~~ + +A service is an object that does something as part of a larger system. Examples +of services: a database connection, a templating engine, or a mailer. Almost +any **global** object can be a service. + +Services are defined by **anonymous functions** that return an instance of an +object: + +.. code-block:: php + + // define some services + $container['session_storage'] = function ($c) { + return new SessionStorage('SESSION_ID'); + }; + + $container['session'] = function ($c) { + return new Session($c['session_storage']); + }; + +Notice that the anonymous function has access to the current container +instance, allowing references to other services or parameters. + +As objects are only created when you get them, the order of the definitions +does not matter. + +Using the defined services is also very easy: + +.. code-block:: php + + // get the session object + $session = $container['session']; + + // the above call is roughly equivalent to the following code: + // $storage = new SessionStorage('SESSION_ID'); + // $session = new Session($storage); + +Defining Factory Services +~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, each time you get a service, Pimple returns the **same instance** +of it. If you want a different instance to be returned for all calls, wrap your +anonymous function with the ``factory()`` method + +.. code-block:: php + + $container['session'] = $container->factory(function ($c) { + return new Session($c['session_storage']); + }); + +Now, each call to ``$container['session']`` returns a new instance of the +session. + +Defining Parameters +~~~~~~~~~~~~~~~~~~~ + +Defining a parameter allows to ease the configuration of your container from +the outside and to store global values: + +.. code-block:: php + + // define some parameters + $container['cookie_name'] = 'SESSION_ID'; + $container['session_storage_class'] = 'SessionStorage'; + +If you change the ``session_storage`` service definition like below: + +.. code-block:: php + + $container['session_storage'] = function ($c) { + return new $c['session_storage_class']($c['cookie_name']); + }; + +You can now easily change the cookie name by overriding the +``session_storage_class`` parameter instead of redefining the service +definition. + +Protecting Parameters +~~~~~~~~~~~~~~~~~~~~~ + +Because Pimple sees anonymous functions as service definitions, you need to +wrap anonymous functions with the ``protect()`` method to store them as +parameters: + +.. code-block:: php + + $container['random_func'] = $container->protect(function () { + return rand(); + }); + +Modifying Services after Definition +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In some cases you may want to modify a service definition after it has been +defined. You can use the ``extend()`` method to define additional code to be +run on your service just after it is created: + +.. code-block:: php + + $container['session_storage'] = function ($c) { + return new $c['session_storage_class']($c['cookie_name']); + }; + + $container->extend('session_storage', function ($storage, $c) { + $storage->...(); + + return $storage; + }); + +The first argument is the name of the service to extend, the second a function +that gets access to the object instance and the container. + +Extending a Container +~~~~~~~~~~~~~~~~~~~~~ + +If you use the same libraries over and over, you might want to reuse some +services from one project to the next one; package your services into a +**provider** by implementing ``Pimple\ServiceProviderInterface``: + +.. code-block:: php + + use Pimple\Container; + + class FooProvider implements Pimple\ServiceProviderInterface + { + public function register(Container $pimple) + { + // register some services and parameters + // on $pimple + } + } + +Then, register the provider on a Container: + +.. code-block:: php + + $pimple->register(new FooProvider()); + +Fetching the Service Creation Function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When you access an object, Pimple automatically calls the anonymous function +that you defined, which creates the service object for you. If you want to get +raw access to this function, you can use the ``raw()`` method: + +.. code-block:: php + + $container['session'] = function ($c) { + return new Session($c['session_storage']); + }; + + $sessionFunction = $container->raw('session'); + +.. _Pimple 1.x documentation: https://github.com/silexphp/Pimple/tree/1.1 diff --git a/vendor/pimple/pimple/composer.json b/vendor/pimple/pimple/composer.json new file mode 100644 index 0000000..a5268f1 --- /dev/null +++ b/vendor/pimple/pimple/composer.json @@ -0,0 +1,25 @@ +{ + "name": "pimple/pimple", + "type": "library", + "description": "Pimple, a simple Dependency Injection Container", + "keywords": ["dependency injection", "container"], + "homepage": "http://pimple.sensiolabs.org", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-0": { "Pimple": "src/" } + }, + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + } +} diff --git a/vendor/pimple/pimple/ext/pimple/.gitignore b/vendor/pimple/pimple/ext/pimple/.gitignore new file mode 100644 index 0000000..1861088 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/.gitignore @@ -0,0 +1,30 @@ +*.sw* +.deps +Makefile +Makefile.fragments +Makefile.global +Makefile.objects +acinclude.m4 +aclocal.m4 +build/ +config.cache +config.guess +config.h +config.h.in +config.log +config.nice +config.status +config.sub +configure +configure.in +install-sh +libtool +ltmain.sh +missing +mkinstalldirs +run-tests.php +*.loT +.libs/ +modules/ +*.la +*.lo diff --git a/vendor/pimple/pimple/ext/pimple/README.md b/vendor/pimple/pimple/ext/pimple/README.md new file mode 100644 index 0000000..7b39eb2 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/README.md @@ -0,0 +1,12 @@ +This is Pimple 2 implemented in C + +* PHP >= 5.3 +* Not tested under Windows, might work + +Install +======= + + > phpize + > ./configure + > make + > make install diff --git a/vendor/pimple/pimple/ext/pimple/config.m4 b/vendor/pimple/pimple/ext/pimple/config.m4 new file mode 100644 index 0000000..c9ba17d --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/config.m4 @@ -0,0 +1,63 @@ +dnl $Id$ +dnl config.m4 for extension pimple + +dnl Comments in this file start with the string 'dnl'. +dnl Remove where necessary. This file will not work +dnl without editing. + +dnl If your extension references something external, use with: + +dnl PHP_ARG_WITH(pimple, for pimple support, +dnl Make sure that the comment is aligned: +dnl [ --with-pimple Include pimple support]) + +dnl Otherwise use enable: + +PHP_ARG_ENABLE(pimple, whether to enable pimple support, +dnl Make sure that the comment is aligned: +[ --enable-pimple Enable pimple support]) + +if test "$PHP_PIMPLE" != "no"; then + dnl Write more examples of tests here... + + dnl # --with-pimple -> check with-path + dnl SEARCH_PATH="/usr/local /usr" # you might want to change this + dnl SEARCH_FOR="/include/pimple.h" # you most likely want to change this + dnl if test -r $PHP_PIMPLE/$SEARCH_FOR; then # path given as parameter + dnl PIMPLE_DIR=$PHP_PIMPLE + dnl else # search default path list + dnl AC_MSG_CHECKING([for pimple files in default path]) + dnl for i in $SEARCH_PATH ; do + dnl if test -r $i/$SEARCH_FOR; then + dnl PIMPLE_DIR=$i + dnl AC_MSG_RESULT(found in $i) + dnl fi + dnl done + dnl fi + dnl + dnl if test -z "$PIMPLE_DIR"; then + dnl AC_MSG_RESULT([not found]) + dnl AC_MSG_ERROR([Please reinstall the pimple distribution]) + dnl fi + + dnl # --with-pimple -> add include path + dnl PHP_ADD_INCLUDE($PIMPLE_DIR/include) + + dnl # --with-pimple -> check for lib and symbol presence + dnl LIBNAME=pimple # you may want to change this + dnl LIBSYMBOL=pimple # you most likely want to change this + + dnl PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL, + dnl [ + dnl PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $PIMPLE_DIR/lib, PIMPLE_SHARED_LIBADD) + dnl AC_DEFINE(HAVE_PIMPLELIB,1,[ ]) + dnl ],[ + dnl AC_MSG_ERROR([wrong pimple lib version or lib not found]) + dnl ],[ + dnl -L$PIMPLE_DIR/lib -lm + dnl ]) + dnl + dnl PHP_SUBST(PIMPLE_SHARED_LIBADD) + + PHP_NEW_EXTENSION(pimple, pimple.c, $ext_shared) +fi diff --git a/vendor/pimple/pimple/ext/pimple/config.w32 b/vendor/pimple/pimple/ext/pimple/config.w32 new file mode 100644 index 0000000..39857b3 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/config.w32 @@ -0,0 +1,13 @@ +// $Id$ +// vim:ft=javascript + +// If your extension references something external, use ARG_WITH +// ARG_WITH("pimple", "for pimple support", "no"); + +// Otherwise, use ARG_ENABLE +// ARG_ENABLE("pimple", "enable pimple support", "no"); + +if (PHP_PIMPLE != "no") { + EXTENSION("pimple", "pimple.c"); +} + diff --git a/vendor/pimple/pimple/ext/pimple/php_pimple.h b/vendor/pimple/pimple/ext/pimple/php_pimple.h new file mode 100644 index 0000000..49431f0 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/php_pimple.h @@ -0,0 +1,121 @@ + +/* + * This file is part of Pimple. + * + * Copyright (c) 2014 Fabien Potencier + * + * 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: + * + * 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. + */ + +#ifndef PHP_PIMPLE_H +#define PHP_PIMPLE_H + +extern zend_module_entry pimple_module_entry; +#define phpext_pimple_ptr &pimple_module_entry + +#ifdef PHP_WIN32 +# define PHP_PIMPLE_API __declspec(dllexport) +#elif defined(__GNUC__) && __GNUC__ >= 4 +# define PHP_PIMPLE_API __attribute__ ((visibility("default"))) +#else +# define PHP_PIMPLE_API +#endif + +#ifdef ZTS +#include "TSRM.h" +#endif + +#define PIMPLE_VERSION "3.0.2" +#define PIMPLE_NS "Pimple" + +#define PIMPLE_DEFAULT_ZVAL_CACHE_NUM 5 +#define PIMPLE_DEFAULT_ZVAL_VALUES_NUM 10 + +zend_module_entry *get_module(void); + +PHP_MINIT_FUNCTION(pimple); +PHP_MINFO_FUNCTION(pimple); + +PHP_METHOD(Pimple, __construct); +PHP_METHOD(Pimple, factory); +PHP_METHOD(Pimple, protect); +PHP_METHOD(Pimple, raw); +PHP_METHOD(Pimple, extend); +PHP_METHOD(Pimple, keys); +PHP_METHOD(Pimple, register); +PHP_METHOD(Pimple, offsetSet); +PHP_METHOD(Pimple, offsetUnset); +PHP_METHOD(Pimple, offsetGet); +PHP_METHOD(Pimple, offsetExists); + +PHP_METHOD(PimpleClosure, invoker); + +typedef struct _pimple_bucket_value { + zval *value; /* Must be the first element */ + zval *raw; + zend_object_handle handle_num; + enum { + PIMPLE_IS_PARAM = 0, + PIMPLE_IS_SERVICE = 2 + } type; + zend_bool initialized; + zend_fcall_info_cache fcc; +} pimple_bucket_value; + +typedef struct _pimple_object { + zend_object zobj; + HashTable values; + HashTable factories; + HashTable protected; +} pimple_object; + +typedef struct _pimple_closure_object { + zend_object zobj; + zval *callable; + zval *factory; +} pimple_closure_object; + +static const char sensiolabs_logo[] = ""; + +static int pimple_zval_to_pimpleval(zval *_zval, pimple_bucket_value *_pimple_bucket_value TSRMLS_DC); +static int pimple_zval_is_valid_callback(zval *_zval, pimple_bucket_value *_pimple_bucket_value TSRMLS_DC); + +static void pimple_bucket_dtor(pimple_bucket_value *bucket); +static void pimple_free_bucket(pimple_bucket_value *bucket); + +static zval *pimple_object_read_dimension(zval *object, zval *offset, int type TSRMLS_DC); +static void pimple_object_write_dimension(zval *object, zval *offset, zval *value TSRMLS_DC); +static int pimple_object_has_dimension(zval *object, zval *offset, int check_empty TSRMLS_DC); +static void pimple_object_unset_dimension(zval *object, zval *offset TSRMLS_DC); +static zend_object_value pimple_object_create(zend_class_entry *ce TSRMLS_DC); +static void pimple_free_object_storage(pimple_object *obj TSRMLS_DC); + +static void pimple_closure_free_object_storage(pimple_closure_object *obj TSRMLS_DC); +static zend_object_value pimple_closure_object_create(zend_class_entry *ce TSRMLS_DC); +static zend_function *pimple_closure_get_constructor(zval * TSRMLS_DC); +static int pimple_closure_get_closure(zval *obj, zend_class_entry **ce_ptr, union _zend_function **fptr_ptr, zval **zobj_ptr TSRMLS_DC); + +#ifdef ZTS +#define PIMPLE_G(v) TSRMG(pimple_globals_id, zend_pimple_globals *, v) +#else +#define PIMPLE_G(v) (pimple_globals.v) +#endif + +#endif /* PHP_PIMPLE_H */ + diff --git a/vendor/pimple/pimple/ext/pimple/pimple.c b/vendor/pimple/pimple/ext/pimple/pimple.c new file mode 100644 index 0000000..239c01d --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/pimple.c @@ -0,0 +1,922 @@ + +/* + * This file is part of Pimple. + * + * Copyright (c) 2014 Fabien Potencier + * + * 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: + * + * 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "php.h" +#include "php_ini.h" +#include "ext/standard/info.h" +#include "php_pimple.h" +#include "pimple_compat.h" +#include "zend_interfaces.h" +#include "zend.h" +#include "Zend/zend_closures.h" +#include "ext/spl/spl_exceptions.h" +#include "Zend/zend_exceptions.h" +#include "main/php_output.h" +#include "SAPI.h" + +static zend_class_entry *pimple_ce; +static zend_object_handlers pimple_object_handlers; +static zend_class_entry *pimple_closure_ce; +static zend_class_entry *pimple_serviceprovider_ce; +static zend_object_handlers pimple_closure_object_handlers; +static zend_internal_function pimple_closure_invoker_function; + +#define FETCH_DIM_HANDLERS_VARS pimple_object *pimple_obj = NULL; \ + ulong index; \ + pimple_obj = (pimple_object *)zend_object_store_get_object(object TSRMLS_CC); \ + +#define PIMPLE_OBJECT_HANDLE_INHERITANCE_OBJECT_HANDLERS do { \ + if (ce != pimple_ce) { \ + zend_hash_find(&ce->function_table, ZEND_STRS("offsetget"), (void **)&function); \ + if (function->common.scope != ce) { /* if the function is not defined in this actual class */ \ + pimple_object_handlers.read_dimension = pimple_object_read_dimension; /* then overwrite the handler to use custom one */ \ + } \ + zend_hash_find(&ce->function_table, ZEND_STRS("offsetset"), (void **)&function); \ + if (function->common.scope != ce) { \ + pimple_object_handlers.write_dimension = pimple_object_write_dimension; \ + } \ + zend_hash_find(&ce->function_table, ZEND_STRS("offsetexists"), (void **)&function); \ + if (function->common.scope != ce) { \ + pimple_object_handlers.has_dimension = pimple_object_has_dimension; \ + } \ + zend_hash_find(&ce->function_table, ZEND_STRS("offsetunset"), (void **)&function); \ + if (function->common.scope != ce) { \ + pimple_object_handlers.unset_dimension = pimple_object_unset_dimension; \ + } \ + } else { \ + pimple_object_handlers.read_dimension = pimple_object_read_dimension; \ + pimple_object_handlers.write_dimension = pimple_object_write_dimension; \ + pimple_object_handlers.has_dimension = pimple_object_has_dimension; \ + pimple_object_handlers.unset_dimension = pimple_object_unset_dimension; \ + }\ + } while(0); + +#define PIMPLE_CALL_CB do { \ + zend_fcall_info_argn(&fci TSRMLS_CC, 1, &object); \ + fci.size = sizeof(fci); \ + fci.object_ptr = retval->fcc.object_ptr; \ + fci.function_name = retval->value; \ + fci.no_separation = 1; \ + fci.retval_ptr_ptr = &retval_ptr_ptr; \ +\ + zend_call_function(&fci, &retval->fcc TSRMLS_CC); \ + efree(fci.params); \ + if (EG(exception)) { \ + return EG(uninitialized_zval_ptr); \ + } \ + } while(0); + +ZEND_BEGIN_ARG_INFO_EX(arginfo___construct, 0, 0, 0) +ZEND_ARG_ARRAY_INFO(0, value, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_offsetset, 0, 0, 2) +ZEND_ARG_INFO(0, offset) +ZEND_ARG_INFO(0, value) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_offsetget, 0, 0, 1) +ZEND_ARG_INFO(0, offset) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_offsetexists, 0, 0, 1) +ZEND_ARG_INFO(0, offset) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_offsetunset, 0, 0, 1) +ZEND_ARG_INFO(0, offset) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_factory, 0, 0, 1) +ZEND_ARG_INFO(0, callable) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_protect, 0, 0, 1) +ZEND_ARG_INFO(0, callable) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_raw, 0, 0, 1) +ZEND_ARG_INFO(0, id) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_extend, 0, 0, 2) +ZEND_ARG_INFO(0, id) +ZEND_ARG_INFO(0, callable) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_keys, 0, 0, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_register, 0, 0, 1) +ZEND_ARG_OBJ_INFO(0, provider, Pimple\\ServiceProviderInterface, 0) +ZEND_ARG_ARRAY_INFO(0, values, 1) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_serviceprovider_register, 0, 0, 1) +ZEND_ARG_OBJ_INFO(0, pimple, Pimple\\Container, 0) +ZEND_END_ARG_INFO() + +static const zend_function_entry pimple_ce_functions[] = { + PHP_ME(Pimple, __construct, arginfo___construct, ZEND_ACC_PUBLIC) + PHP_ME(Pimple, factory, arginfo_factory, ZEND_ACC_PUBLIC) + PHP_ME(Pimple, protect, arginfo_protect, ZEND_ACC_PUBLIC) + PHP_ME(Pimple, raw, arginfo_raw, ZEND_ACC_PUBLIC) + PHP_ME(Pimple, extend, arginfo_extend, ZEND_ACC_PUBLIC) + PHP_ME(Pimple, keys, arginfo_keys, ZEND_ACC_PUBLIC) + PHP_ME(Pimple, register, arginfo_register, ZEND_ACC_PUBLIC) + + PHP_ME(Pimple, offsetSet, arginfo_offsetset, ZEND_ACC_PUBLIC) + PHP_ME(Pimple, offsetGet, arginfo_offsetget, ZEND_ACC_PUBLIC) + PHP_ME(Pimple, offsetExists, arginfo_offsetexists, ZEND_ACC_PUBLIC) + PHP_ME(Pimple, offsetUnset, arginfo_offsetunset, ZEND_ACC_PUBLIC) + PHP_FE_END +}; + +static const zend_function_entry pimple_serviceprovider_iface_ce_functions[] = { + PHP_ABSTRACT_ME(ServiceProviderInterface, register, arginfo_serviceprovider_register) + PHP_FE_END +}; + +static void pimple_closure_free_object_storage(pimple_closure_object *obj TSRMLS_DC) +{ + zend_object_std_dtor(&obj->zobj TSRMLS_CC); + if (obj->factory) { + zval_ptr_dtor(&obj->factory); + } + if (obj->callable) { + zval_ptr_dtor(&obj->callable); + } + efree(obj); +} + +static void pimple_free_object_storage(pimple_object *obj TSRMLS_DC) +{ + zend_hash_destroy(&obj->factories); + zend_hash_destroy(&obj->protected); + zend_hash_destroy(&obj->values); + zend_object_std_dtor(&obj->zobj TSRMLS_CC); + efree(obj); +} + +static void pimple_free_bucket(pimple_bucket_value *bucket) +{ + if (bucket->raw) { + zval_ptr_dtor(&bucket->raw); + } +} + +static zend_object_value pimple_closure_object_create(zend_class_entry *ce TSRMLS_DC) +{ + zend_object_value retval; + pimple_closure_object *pimple_closure_obj = NULL; + + pimple_closure_obj = ecalloc(1, sizeof(pimple_closure_object)); + ZEND_OBJ_INIT(&pimple_closure_obj->zobj, ce); + + pimple_closure_object_handlers.get_constructor = pimple_closure_get_constructor; + retval.handlers = &pimple_closure_object_handlers; + retval.handle = zend_objects_store_put(pimple_closure_obj, (zend_objects_store_dtor_t) zend_objects_destroy_object, (zend_objects_free_object_storage_t) pimple_closure_free_object_storage, NULL TSRMLS_CC); + + return retval; +} + +static zend_function *pimple_closure_get_constructor(zval *obj TSRMLS_DC) +{ + zend_error(E_ERROR, "Pimple\\ContainerClosure is an internal class and cannot be instantiated"); + + return NULL; +} + +static int pimple_closure_get_closure(zval *obj, zend_class_entry **ce_ptr, union _zend_function **fptr_ptr, zval **zobj_ptr TSRMLS_DC) +{ + *zobj_ptr = obj; + *ce_ptr = Z_OBJCE_P(obj); + *fptr_ptr = (zend_function *)&pimple_closure_invoker_function; + + return SUCCESS; +} + +static zend_object_value pimple_object_create(zend_class_entry *ce TSRMLS_DC) +{ + zend_object_value retval; + pimple_object *pimple_obj = NULL; + zend_function *function = NULL; + + pimple_obj = emalloc(sizeof(pimple_object)); + ZEND_OBJ_INIT(&pimple_obj->zobj, ce); + + PIMPLE_OBJECT_HANDLE_INHERITANCE_OBJECT_HANDLERS + + retval.handlers = &pimple_object_handlers; + retval.handle = zend_objects_store_put(pimple_obj, (zend_objects_store_dtor_t) zend_objects_destroy_object, (zend_objects_free_object_storage_t) pimple_free_object_storage, NULL TSRMLS_CC); + + zend_hash_init(&pimple_obj->factories, PIMPLE_DEFAULT_ZVAL_CACHE_NUM, NULL, (dtor_func_t)pimple_bucket_dtor, 0); + zend_hash_init(&pimple_obj->protected, PIMPLE_DEFAULT_ZVAL_CACHE_NUM, NULL, (dtor_func_t)pimple_bucket_dtor, 0); + zend_hash_init(&pimple_obj->values, PIMPLE_DEFAULT_ZVAL_VALUES_NUM, NULL, (dtor_func_t)pimple_bucket_dtor, 0); + + return retval; +} + +static void pimple_object_write_dimension(zval *object, zval *offset, zval *value TSRMLS_DC) +{ + FETCH_DIM_HANDLERS_VARS + + pimple_bucket_value pimple_value = {0}, *found_value = NULL; + ulong hash; + + pimple_zval_to_pimpleval(value, &pimple_value TSRMLS_CC); + + if (!offset) {/* $p[] = 'foo' when not overloaded */ + zend_hash_next_index_insert(&pimple_obj->values, (void *)&pimple_value, sizeof(pimple_bucket_value), NULL); + Z_ADDREF_P(value); + return; + } + + switch (Z_TYPE_P(offset)) { + case IS_STRING: + hash = zend_hash_func(Z_STRVAL_P(offset), Z_STRLEN_P(offset)+1); + zend_hash_quick_find(&pimple_obj->values, Z_STRVAL_P(offset), Z_STRLEN_P(offset)+1, hash, (void **)&found_value); + if (found_value && found_value->type == PIMPLE_IS_SERVICE && found_value->initialized == 1) { + pimple_free_bucket(&pimple_value); + zend_throw_exception_ex(spl_ce_RuntimeException, 0 TSRMLS_CC, "Cannot override frozen service \"%s\".", Z_STRVAL_P(offset)); + return; + } + if (zend_hash_quick_update(&pimple_obj->values, Z_STRVAL_P(offset), Z_STRLEN_P(offset)+1, hash, (void *)&pimple_value, sizeof(pimple_bucket_value), NULL) == FAILURE) { + pimple_free_bucket(&pimple_value); + return; + } + Z_ADDREF_P(value); + break; + case IS_DOUBLE: + case IS_BOOL: + case IS_LONG: + if (Z_TYPE_P(offset) == IS_DOUBLE) { + index = (ulong)Z_DVAL_P(offset); + } else { + index = Z_LVAL_P(offset); + } + zend_hash_index_find(&pimple_obj->values, index, (void **)&found_value); + if (found_value && found_value->type == PIMPLE_IS_SERVICE && found_value->initialized == 1) { + pimple_free_bucket(&pimple_value); + zend_throw_exception_ex(spl_ce_RuntimeException, 0 TSRMLS_CC, "Cannot override frozen service \"%ld\".", index); + return; + } + if (zend_hash_index_update(&pimple_obj->values, index, (void *)&pimple_value, sizeof(pimple_bucket_value), NULL) == FAILURE) { + pimple_free_bucket(&pimple_value); + return; + } + Z_ADDREF_P(value); + break; + case IS_NULL: /* $p[] = 'foo' when overloaded */ + zend_hash_next_index_insert(&pimple_obj->values, (void *)&pimple_value, sizeof(pimple_bucket_value), NULL); + Z_ADDREF_P(value); + break; + default: + pimple_free_bucket(&pimple_value); + zend_error(E_WARNING, "Unsupported offset type"); + } +} + +static void pimple_object_unset_dimension(zval *object, zval *offset TSRMLS_DC) +{ + FETCH_DIM_HANDLERS_VARS + + switch (Z_TYPE_P(offset)) { + case IS_STRING: + zend_symtable_del(&pimple_obj->values, Z_STRVAL_P(offset), Z_STRLEN_P(offset)+1); + zend_symtable_del(&pimple_obj->factories, Z_STRVAL_P(offset), Z_STRLEN_P(offset)+1); + zend_symtable_del(&pimple_obj->protected, Z_STRVAL_P(offset), Z_STRLEN_P(offset)+1); + break; + case IS_DOUBLE: + case IS_BOOL: + case IS_LONG: + if (Z_TYPE_P(offset) == IS_DOUBLE) { + index = (ulong)Z_DVAL_P(offset); + } else { + index = Z_LVAL_P(offset); + } + zend_hash_index_del(&pimple_obj->values, index); + zend_hash_index_del(&pimple_obj->factories, index); + zend_hash_index_del(&pimple_obj->protected, index); + break; + default: + zend_error(E_WARNING, "Unsupported offset type"); + } +} + +static int pimple_object_has_dimension(zval *object, zval *offset, int check_empty TSRMLS_DC) +{ + FETCH_DIM_HANDLERS_VARS + + pimple_bucket_value *retval = NULL; + + switch (Z_TYPE_P(offset)) { + case IS_STRING: + if (zend_symtable_find(&pimple_obj->values, Z_STRVAL_P(offset), Z_STRLEN_P(offset)+1, (void **)&retval) == SUCCESS) { + switch (check_empty) { + case 0: /* isset */ + return 1; /* Differs from PHP behavior (Z_TYPE_P(retval->value) != IS_NULL;) */ + case 1: /* empty */ + default: + return zend_is_true(retval->value); + } + } + return 0; + break; + case IS_DOUBLE: + case IS_BOOL: + case IS_LONG: + if (Z_TYPE_P(offset) == IS_DOUBLE) { + index = (ulong)Z_DVAL_P(offset); + } else { + index = Z_LVAL_P(offset); + } + if (zend_hash_index_find(&pimple_obj->values, index, (void **)&retval) == SUCCESS) { + switch (check_empty) { + case 0: /* isset */ + return 1; /* Differs from PHP behavior (Z_TYPE_P(retval->value) != IS_NULL;)*/ + case 1: /* empty */ + default: + return zend_is_true(retval->value); + } + } + return 0; + break; + default: + zend_error(E_WARNING, "Unsupported offset type"); + return 0; + } +} + +static zval *pimple_object_read_dimension(zval *object, zval *offset, int type TSRMLS_DC) +{ + FETCH_DIM_HANDLERS_VARS + + pimple_bucket_value *retval = NULL; + zend_fcall_info fci = {0}; + zval *retval_ptr_ptr = NULL; + + switch (Z_TYPE_P(offset)) { + case IS_STRING: + if (zend_symtable_find(&pimple_obj->values, Z_STRVAL_P(offset), Z_STRLEN_P(offset)+1, (void **)&retval) == FAILURE) { + zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0 TSRMLS_CC, "Identifier \"%s\" is not defined.", Z_STRVAL_P(offset)); + return EG(uninitialized_zval_ptr); + } + break; + case IS_DOUBLE: + case IS_BOOL: + case IS_LONG: + if (Z_TYPE_P(offset) == IS_DOUBLE) { + index = (ulong)Z_DVAL_P(offset); + } else { + index = Z_LVAL_P(offset); + } + if (zend_hash_index_find(&pimple_obj->values, index, (void **)&retval) == FAILURE) { + return EG(uninitialized_zval_ptr); + } + break; + case IS_NULL: /* $p[][3] = 'foo' first dim access */ + return EG(uninitialized_zval_ptr); + break; + default: + zend_error(E_WARNING, "Unsupported offset type"); + return EG(uninitialized_zval_ptr); + } + + if(retval->type == PIMPLE_IS_PARAM) { + return retval->value; + } + + if (zend_hash_index_exists(&pimple_obj->protected, retval->handle_num)) { + /* Service is protected, return the value every time */ + return retval->value; + } + + if (zend_hash_index_exists(&pimple_obj->factories, retval->handle_num)) { + /* Service is a factory, call it everytime and never cache its result */ + PIMPLE_CALL_CB + Z_DELREF_P(retval_ptr_ptr); /* fetch dim addr will increment refcount */ + return retval_ptr_ptr; + } + + if (retval->initialized == 1) { + /* Service has already been called, return its cached value */ + return retval->value; + } + + ALLOC_INIT_ZVAL(retval->raw); + MAKE_COPY_ZVAL(&retval->value, retval->raw); + + PIMPLE_CALL_CB + + retval->initialized = 1; + zval_ptr_dtor(&retval->value); + retval->value = retval_ptr_ptr; + + return retval->value; +} + +static int pimple_zval_is_valid_callback(zval *_zval, pimple_bucket_value *_pimple_bucket_value TSRMLS_DC) +{ + if (Z_TYPE_P(_zval) != IS_OBJECT) { + return FAILURE; + } + + if (_pimple_bucket_value->fcc.called_scope) { + return SUCCESS; + } + + if (Z_OBJ_HANDLER_P(_zval, get_closure) && Z_OBJ_HANDLER_P(_zval, get_closure)(_zval, &_pimple_bucket_value->fcc.calling_scope, &_pimple_bucket_value->fcc.function_handler, &_pimple_bucket_value->fcc.object_ptr TSRMLS_CC) == SUCCESS) { + _pimple_bucket_value->fcc.called_scope = _pimple_bucket_value->fcc.calling_scope; + return SUCCESS; + } else { + return FAILURE; + } +} + +static int pimple_zval_to_pimpleval(zval *_zval, pimple_bucket_value *_pimple_bucket_value TSRMLS_DC) +{ + _pimple_bucket_value->value = _zval; + + if (Z_TYPE_P(_zval) != IS_OBJECT) { + return PIMPLE_IS_PARAM; + } + + if (pimple_zval_is_valid_callback(_zval, _pimple_bucket_value TSRMLS_CC) == SUCCESS) { + _pimple_bucket_value->type = PIMPLE_IS_SERVICE; + _pimple_bucket_value->handle_num = Z_OBJ_HANDLE_P(_zval); + } + + return PIMPLE_IS_SERVICE; +} + +static void pimple_bucket_dtor(pimple_bucket_value *bucket) +{ + zval_ptr_dtor(&bucket->value); + pimple_free_bucket(bucket); +} + +PHP_METHOD(Pimple, protect) +{ + zval *protected = NULL; + pimple_object *pobj = NULL; + pimple_bucket_value bucket = {0}; + + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &protected) == FAILURE) { + return; + } + + if (pimple_zval_is_valid_callback(protected, &bucket TSRMLS_CC) == FAILURE) { + pimple_free_bucket(&bucket); + zend_throw_exception(spl_ce_InvalidArgumentException, "Callable is not a Closure or invokable object.", 0 TSRMLS_CC); + return; + } + + pimple_zval_to_pimpleval(protected, &bucket TSRMLS_CC); + pobj = (pimple_object *)zend_object_store_get_object(getThis() TSRMLS_CC); + + if (zend_hash_index_update(&pobj->protected, bucket.handle_num, (void *)&bucket, sizeof(pimple_bucket_value), NULL) == SUCCESS) { + Z_ADDREF_P(protected); + RETURN_ZVAL(protected, 1 , 0); + } else { + pimple_free_bucket(&bucket); + } + RETURN_FALSE; +} + +PHP_METHOD(Pimple, raw) +{ + zval *offset = NULL; + pimple_object *pobj = NULL; + pimple_bucket_value *value = NULL; + ulong index; + + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &offset) == FAILURE) { + return; + } + + pobj = zend_object_store_get_object(getThis() TSRMLS_CC); + + switch (Z_TYPE_P(offset)) { + case IS_STRING: + if (zend_symtable_find(&pobj->values, Z_STRVAL_P(offset), Z_STRLEN_P(offset)+1, (void *)&value) == FAILURE) { + zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0 TSRMLS_CC, "Identifier \"%s\" is not defined.", Z_STRVAL_P(offset)); + RETURN_NULL(); + } + break; + case IS_DOUBLE: + case IS_BOOL: + case IS_LONG: + if (Z_TYPE_P(offset) == IS_DOUBLE) { + index = (ulong)Z_DVAL_P(offset); + } else { + index = Z_LVAL_P(offset); + } + if (zend_hash_index_find(&pobj->values, index, (void *)&value) == FAILURE) { + RETURN_NULL(); + } + break; + case IS_NULL: + default: + zend_error(E_WARNING, "Unsupported offset type"); + } + + if (value->raw) { + RETVAL_ZVAL(value->raw, 1, 0); + } else { + RETVAL_ZVAL(value->value, 1, 0); + } +} + +PHP_METHOD(Pimple, extend) +{ + zval *offset = NULL, *callable = NULL, *pimple_closure_obj = NULL; + pimple_bucket_value bucket = {0}, *value = NULL; + pimple_object *pobj = NULL; + pimple_closure_object *pcobj = NULL; + ulong index; + + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "zz", &offset, &callable) == FAILURE) { + return; + } + + pobj = zend_object_store_get_object(getThis() TSRMLS_CC); + + switch (Z_TYPE_P(offset)) { + case IS_STRING: + if (zend_symtable_find(&pobj->values, Z_STRVAL_P(offset), Z_STRLEN_P(offset)+1, (void *)&value) == FAILURE) { + zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0 TSRMLS_CC, "Identifier \"%s\" is not defined.", Z_STRVAL_P(offset)); + RETURN_NULL(); + } + if (value->type != PIMPLE_IS_SERVICE) { + zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0 TSRMLS_CC, "Identifier \"%s\" does not contain an object definition.", Z_STRVAL_P(offset)); + RETURN_NULL(); + } + break; + case IS_DOUBLE: + case IS_BOOL: + case IS_LONG: + if (Z_TYPE_P(offset) == IS_DOUBLE) { + index = (ulong)Z_DVAL_P(offset); + } else { + index = Z_LVAL_P(offset); + } + if (zend_hash_index_find(&pobj->values, index, (void *)&value) == FAILURE) { + zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0 TSRMLS_CC, "Identifier \"%ld\" is not defined.", index); + RETURN_NULL(); + } + if (value->type != PIMPLE_IS_SERVICE) { + zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0 TSRMLS_CC, "Identifier \"%ld\" does not contain an object definition.", index); + RETURN_NULL(); + } + break; + case IS_NULL: + default: + zend_error(E_WARNING, "Unsupported offset type"); + } + + if (pimple_zval_is_valid_callback(callable, &bucket TSRMLS_CC) == FAILURE) { + pimple_free_bucket(&bucket); + zend_throw_exception(spl_ce_InvalidArgumentException, "Extension service definition is not a Closure or invokable object.", 0 TSRMLS_CC); + RETURN_NULL(); + } + pimple_free_bucket(&bucket); + + ALLOC_INIT_ZVAL(pimple_closure_obj); + object_init_ex(pimple_closure_obj, pimple_closure_ce); + + pcobj = zend_object_store_get_object(pimple_closure_obj TSRMLS_CC); + pcobj->callable = callable; + pcobj->factory = value->value; + Z_ADDREF_P(callable); + Z_ADDREF_P(value->value); + + if (zend_hash_index_exists(&pobj->factories, value->handle_num)) { + pimple_zval_to_pimpleval(pimple_closure_obj, &bucket TSRMLS_CC); + zend_hash_index_del(&pobj->factories, value->handle_num); + zend_hash_index_update(&pobj->factories, bucket.handle_num, (void *)&bucket, sizeof(pimple_bucket_value), NULL); + Z_ADDREF_P(pimple_closure_obj); + } + + pimple_object_write_dimension(getThis(), offset, pimple_closure_obj TSRMLS_CC); + + RETVAL_ZVAL(pimple_closure_obj, 1, 1); +} + +PHP_METHOD(Pimple, keys) +{ + HashPosition pos; + pimple_object *pobj = NULL; + zval **value = NULL; + zval *endval = NULL; + char *str_index = NULL; + int str_len; + ulong num_index; + + if (zend_parse_parameters_none() == FAILURE) { + return; + } + + pobj = zend_object_store_get_object(getThis() TSRMLS_CC); + array_init_size(return_value, zend_hash_num_elements(&pobj->values)); + + zend_hash_internal_pointer_reset_ex(&pobj->values, &pos); + + while(zend_hash_get_current_data_ex(&pobj->values, (void **)&value, &pos) == SUCCESS) { + MAKE_STD_ZVAL(endval); + switch (zend_hash_get_current_key_ex(&pobj->values, &str_index, (uint *)&str_len, &num_index, 0, &pos)) { + case HASH_KEY_IS_STRING: + ZVAL_STRINGL(endval, str_index, str_len - 1, 1); + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &endval, sizeof(zval *), NULL); + break; + case HASH_KEY_IS_LONG: + ZVAL_LONG(endval, num_index); + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &endval, sizeof(zval *), NULL); + break; + } + zend_hash_move_forward_ex(&pobj->values, &pos); + } +} + +PHP_METHOD(Pimple, factory) +{ + zval *factory = NULL; + pimple_object *pobj = NULL; + pimple_bucket_value bucket = {0}; + + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &factory) == FAILURE) { + return; + } + + if (pimple_zval_is_valid_callback(factory, &bucket TSRMLS_CC) == FAILURE) { + pimple_free_bucket(&bucket); + zend_throw_exception(spl_ce_InvalidArgumentException, "Service definition is not a Closure or invokable object.", 0 TSRMLS_CC); + return; + } + + pimple_zval_to_pimpleval(factory, &bucket TSRMLS_CC); + pobj = (pimple_object *)zend_object_store_get_object(getThis() TSRMLS_CC); + + if (zend_hash_index_update(&pobj->factories, bucket.handle_num, (void *)&bucket, sizeof(pimple_bucket_value), NULL) == SUCCESS) { + Z_ADDREF_P(factory); + RETURN_ZVAL(factory, 1 , 0); + } else { + pimple_free_bucket(&bucket); + } + + RETURN_FALSE; +} + +PHP_METHOD(Pimple, offsetSet) +{ + zval *offset = NULL, *value = NULL; + + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "zz", &offset, &value) == FAILURE) { + return; + } + + pimple_object_write_dimension(getThis(), offset, value TSRMLS_CC); +} + +PHP_METHOD(Pimple, offsetGet) +{ + zval *offset = NULL, *retval = NULL; + + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &offset) == FAILURE) { + return; + } + + retval = pimple_object_read_dimension(getThis(), offset, 0 TSRMLS_CC); + + RETVAL_ZVAL(retval, 1, 0); +} + +PHP_METHOD(Pimple, offsetUnset) +{ + zval *offset = NULL; + + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &offset) == FAILURE) { + return; + } + + pimple_object_unset_dimension(getThis(), offset TSRMLS_CC); +} + +PHP_METHOD(Pimple, offsetExists) +{ + zval *offset = NULL; + + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &offset) == FAILURE) { + return; + } + + RETVAL_BOOL(pimple_object_has_dimension(getThis(), offset, 1 TSRMLS_CC)); +} + +PHP_METHOD(Pimple, register) +{ + zval *provider; + zval **data; + zval *retval = NULL; + zval key; + + HashTable *array = NULL; + HashPosition pos; + + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "O|h", &provider, pimple_serviceprovider_ce, &array) == FAILURE) { + return; + } + + RETVAL_ZVAL(getThis(), 1, 0); + + zend_call_method_with_1_params(&provider, Z_OBJCE_P(provider), NULL, "register", &retval, getThis()); + + if (retval) { + zval_ptr_dtor(&retval); + } + + if (!array) { + return; + } + + zend_hash_internal_pointer_reset_ex(array, &pos); + + while(zend_hash_get_current_data_ex(array, (void **)&data, &pos) == SUCCESS) { + zend_hash_get_current_key_zval_ex(array, &key, &pos); + pimple_object_write_dimension(getThis(), &key, *data TSRMLS_CC); + zend_hash_move_forward_ex(array, &pos); + } +} + +PHP_METHOD(Pimple, __construct) +{ + zval *values = NULL, **pData = NULL, offset; + HashPosition pos; + char *str_index = NULL; + zend_uint str_length; + ulong num_index; + + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|a!", &values) == FAILURE || !values) { + return; + } + + zend_hash_internal_pointer_reset_ex(Z_ARRVAL_P(values), &pos); + while (zend_hash_has_more_elements_ex(Z_ARRVAL_P(values), &pos) == SUCCESS) { + zend_hash_get_current_data_ex(Z_ARRVAL_P(values), (void **)&pData, &pos); + zend_hash_get_current_key_ex(Z_ARRVAL_P(values), &str_index, &str_length, &num_index, 0, &pos); + INIT_ZVAL(offset); + if (zend_hash_get_current_key_type_ex(Z_ARRVAL_P(values), &pos) == HASH_KEY_IS_LONG) { + ZVAL_LONG(&offset, num_index); + } else { + ZVAL_STRINGL(&offset, str_index, (str_length - 1), 0); + } + pimple_object_write_dimension(getThis(), &offset, *pData TSRMLS_CC); + zend_hash_move_forward_ex(Z_ARRVAL_P(values), &pos); + } +} + +/* + * This is PHP code snippet handling extend()s calls : + + $extended = function ($c) use ($callable, $factory) { + return $callable($factory($c), $c); + }; + + */ +PHP_METHOD(PimpleClosure, invoker) +{ + pimple_closure_object *pcobj = NULL; + zval *arg = NULL, *retval = NULL, *newretval = NULL; + zend_fcall_info fci = {0}; + zval **args[2]; + + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &arg) == FAILURE) { + return; + } + + pcobj = zend_object_store_get_object(getThis() TSRMLS_CC); + + fci.function_name = pcobj->factory; + args[0] = &arg; + zend_fcall_info_argp(&fci TSRMLS_CC, 1, args); + fci.retval_ptr_ptr = &retval; + fci.size = sizeof(fci); + + if (zend_call_function(&fci, NULL TSRMLS_CC) == FAILURE || EG(exception)) { + efree(fci.params); + return; /* Should here return default zval */ + } + + efree(fci.params); + memset(&fci, 0, sizeof(fci)); + fci.size = sizeof(fci); + + fci.function_name = pcobj->callable; + args[0] = &retval; + args[1] = &arg; + zend_fcall_info_argp(&fci TSRMLS_CC, 2, args); + fci.retval_ptr_ptr = &newretval; + + if (zend_call_function(&fci, NULL TSRMLS_CC) == FAILURE || EG(exception)) { + efree(fci.params); + zval_ptr_dtor(&retval); + return; + } + + efree(fci.params); + zval_ptr_dtor(&retval); + + RETVAL_ZVAL(newretval, 1 ,1); +} + +PHP_MINIT_FUNCTION(pimple) +{ + zend_class_entry tmp_pimple_ce, tmp_pimple_closure_ce, tmp_pimple_serviceprovider_iface_ce; + INIT_NS_CLASS_ENTRY(tmp_pimple_ce, PIMPLE_NS, "Container", pimple_ce_functions); + INIT_NS_CLASS_ENTRY(tmp_pimple_closure_ce, PIMPLE_NS, "ContainerClosure", NULL); + INIT_NS_CLASS_ENTRY(tmp_pimple_serviceprovider_iface_ce, PIMPLE_NS, "ServiceProviderInterface", pimple_serviceprovider_iface_ce_functions); + + tmp_pimple_ce.create_object = pimple_object_create; + tmp_pimple_closure_ce.create_object = pimple_closure_object_create; + + pimple_ce = zend_register_internal_class(&tmp_pimple_ce TSRMLS_CC); + zend_class_implements(pimple_ce TSRMLS_CC, 1, zend_ce_arrayaccess); + + pimple_closure_ce = zend_register_internal_class(&tmp_pimple_closure_ce TSRMLS_CC); + pimple_closure_ce->ce_flags |= ZEND_ACC_FINAL_CLASS; + + pimple_serviceprovider_ce = zend_register_internal_interface(&tmp_pimple_serviceprovider_iface_ce TSRMLS_CC); + + memcpy(&pimple_closure_object_handlers, zend_get_std_object_handlers(), sizeof(*zend_get_std_object_handlers())); + pimple_object_handlers = std_object_handlers; + pimple_closure_object_handlers.get_closure = pimple_closure_get_closure; + + pimple_closure_invoker_function.function_name = "Pimple closure internal invoker"; + pimple_closure_invoker_function.fn_flags |= ZEND_ACC_CLOSURE; + pimple_closure_invoker_function.handler = ZEND_MN(PimpleClosure_invoker); + pimple_closure_invoker_function.num_args = 1; + pimple_closure_invoker_function.required_num_args = 1; + pimple_closure_invoker_function.scope = pimple_closure_ce; + pimple_closure_invoker_function.type = ZEND_INTERNAL_FUNCTION; + pimple_closure_invoker_function.module = &pimple_module_entry; + + return SUCCESS; +} + +PHP_MINFO_FUNCTION(pimple) +{ + php_info_print_table_start(); + php_info_print_table_header(2, "SensioLabs Pimple C support", "enabled"); + php_info_print_table_row(2, "Pimple supported version", PIMPLE_VERSION); + php_info_print_table_end(); + + php_info_print_box_start(0); + php_write((void *)ZEND_STRL("SensioLabs Pimple C support developed by Julien Pauli") TSRMLS_CC); + if (!sapi_module.phpinfo_as_text) { + php_write((void *)ZEND_STRL(sensiolabs_logo) TSRMLS_CC); + } + php_info_print_box_end(); +} + +zend_module_entry pimple_module_entry = { + STANDARD_MODULE_HEADER, + "pimple", + NULL, + PHP_MINIT(pimple), + NULL, + NULL, + NULL, + PHP_MINFO(pimple), + PIMPLE_VERSION, + STANDARD_MODULE_PROPERTIES +}; + +#ifdef COMPILE_DL_PIMPLE +ZEND_GET_MODULE(pimple) +#endif diff --git a/vendor/pimple/pimple/ext/pimple/pimple_compat.h b/vendor/pimple/pimple/ext/pimple/pimple_compat.h new file mode 100644 index 0000000..d234e17 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/pimple_compat.h @@ -0,0 +1,81 @@ + +/* + * This file is part of Pimple. + * + * Copyright (c) 2014 Fabien Potencier + * + * 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: + * + * 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. + */ + +#ifndef PIMPLE_COMPAT_H_ +#define PIMPLE_COMPAT_H_ + +#include "Zend/zend_extensions.h" /* for ZEND_EXTENSION_API_NO */ + +#define PHP_5_0_X_API_NO 220040412 +#define PHP_5_1_X_API_NO 220051025 +#define PHP_5_2_X_API_NO 220060519 +#define PHP_5_3_X_API_NO 220090626 +#define PHP_5_4_X_API_NO 220100525 +#define PHP_5_5_X_API_NO 220121212 +#define PHP_5_6_X_API_NO 220131226 + +#define IS_PHP_56 ZEND_EXTENSION_API_NO == PHP_5_6_X_API_NO +#define IS_AT_LEAST_PHP_56 ZEND_EXTENSION_API_NO >= PHP_5_6_X_API_NO + +#define IS_PHP_55 ZEND_EXTENSION_API_NO == PHP_5_5_X_API_NO +#define IS_AT_LEAST_PHP_55 ZEND_EXTENSION_API_NO >= PHP_5_5_X_API_NO + +#define IS_PHP_54 ZEND_EXTENSION_API_NO == PHP_5_4_X_API_NO +#define IS_AT_LEAST_PHP_54 ZEND_EXTENSION_API_NO >= PHP_5_4_X_API_NO + +#define IS_PHP_53 ZEND_EXTENSION_API_NO == PHP_5_3_X_API_NO +#define IS_AT_LEAST_PHP_53 ZEND_EXTENSION_API_NO >= PHP_5_3_X_API_NO + +#if IS_PHP_53 +#define object_properties_init(obj, ce) do { \ + zend_hash_copy(obj->properties, &ce->default_properties, zval_copy_property_ctor(ce), NULL, sizeof(zval *)); \ + } while (0); +#endif + +#define ZEND_OBJ_INIT(obj, ce) do { \ + zend_object_std_init(obj, ce TSRMLS_CC); \ + object_properties_init((obj), (ce)); \ + } while(0); + +#if IS_PHP_53 || IS_PHP_54 +static void zend_hash_get_current_key_zval_ex(const HashTable *ht, zval *key, HashPosition *pos) { + Bucket *p; + + p = pos ? (*pos) : ht->pInternalPointer; + + if (!p) { + Z_TYPE_P(key) = IS_NULL; + } else if (p->nKeyLength) { + Z_TYPE_P(key) = IS_STRING; + Z_STRVAL_P(key) = estrndup(p->arKey, p->nKeyLength - 1); + Z_STRLEN_P(key) = p->nKeyLength - 1; + } else { + Z_TYPE_P(key) = IS_LONG; + Z_LVAL_P(key) = p->h; + } +} +#endif + +#endif /* PIMPLE_COMPAT_H_ */ diff --git a/vendor/pimple/pimple/ext/pimple/tests/001.phpt b/vendor/pimple/pimple/ext/pimple/tests/001.phpt new file mode 100644 index 0000000..0809ea2 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/001.phpt @@ -0,0 +1,45 @@ +--TEST-- +Test for read_dim/write_dim handlers +--SKIPIF-- + +--FILE-- + + +--EXPECTF-- +foo +42 +foo2 +foo99 +baz +strstr \ No newline at end of file diff --git a/vendor/pimple/pimple/ext/pimple/tests/002.phpt b/vendor/pimple/pimple/ext/pimple/tests/002.phpt new file mode 100644 index 0000000..7b56d2c --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/002.phpt @@ -0,0 +1,15 @@ +--TEST-- +Test for constructor +--SKIPIF-- + +--FILE-- +'foo')); +var_dump($p[42]); +?> +--EXPECT-- +NULL +string(3) "foo" diff --git a/vendor/pimple/pimple/ext/pimple/tests/003.phpt b/vendor/pimple/pimple/ext/pimple/tests/003.phpt new file mode 100644 index 0000000..a22cfa3 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/003.phpt @@ -0,0 +1,16 @@ +--TEST-- +Test empty dimensions +--SKIPIF-- + +--FILE-- + +--EXPECT-- +int(42) +string(3) "bar" \ No newline at end of file diff --git a/vendor/pimple/pimple/ext/pimple/tests/004.phpt b/vendor/pimple/pimple/ext/pimple/tests/004.phpt new file mode 100644 index 0000000..1e1d251 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/004.phpt @@ -0,0 +1,30 @@ +--TEST-- +Test has/unset dim handlers +--SKIPIF-- + +--FILE-- + +--EXPECT-- +int(42) +NULL +bool(true) +bool(false) +bool(true) +bool(true) \ No newline at end of file diff --git a/vendor/pimple/pimple/ext/pimple/tests/005.phpt b/vendor/pimple/pimple/ext/pimple/tests/005.phpt new file mode 100644 index 0000000..0479ee0 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/005.phpt @@ -0,0 +1,27 @@ +--TEST-- +Test simple class inheritance +--SKIPIF-- + +--FILE-- +someAttr; +?> +--EXPECT-- +string(3) "hit" +foo +fooAttr \ No newline at end of file diff --git a/vendor/pimple/pimple/ext/pimple/tests/006.phpt b/vendor/pimple/pimple/ext/pimple/tests/006.phpt new file mode 100644 index 0000000..cfe8a11 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/006.phpt @@ -0,0 +1,51 @@ +--TEST-- +Test complex class inheritance +--SKIPIF-- + +--FILE-- + 'bar', 88 => 'baz'); + +$p = new TestPimple($defaultValues); +$p[42] = 'foo'; +var_dump($p[42]); +var_dump($p[0]); +?> +--EXPECT-- +string(13) "hit offsetset" +string(27) "hit offsetget in TestPimple" +string(25) "hit offsetget in MyPimple" +string(3) "foo" +string(27) "hit offsetget in TestPimple" +string(25) "hit offsetget in MyPimple" +string(3) "baz" \ No newline at end of file diff --git a/vendor/pimple/pimple/ext/pimple/tests/007.phpt b/vendor/pimple/pimple/ext/pimple/tests/007.phpt new file mode 100644 index 0000000..5aac683 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/007.phpt @@ -0,0 +1,22 @@ +--TEST-- +Test for read_dim/write_dim handlers +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +foo +42 \ No newline at end of file diff --git a/vendor/pimple/pimple/ext/pimple/tests/008.phpt b/vendor/pimple/pimple/ext/pimple/tests/008.phpt new file mode 100644 index 0000000..db7eeec --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/008.phpt @@ -0,0 +1,29 @@ +--TEST-- +Test frozen services +--SKIPIF-- + +--FILE-- + +--EXPECTF-- diff --git a/vendor/pimple/pimple/ext/pimple/tests/009.phpt b/vendor/pimple/pimple/ext/pimple/tests/009.phpt new file mode 100644 index 0000000..bb05ea2 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/009.phpt @@ -0,0 +1,13 @@ +--TEST-- +Test service is called as callback, and only once +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +bool(true) \ No newline at end of file diff --git a/vendor/pimple/pimple/ext/pimple/tests/010.phpt b/vendor/pimple/pimple/ext/pimple/tests/010.phpt new file mode 100644 index 0000000..badce01 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/010.phpt @@ -0,0 +1,45 @@ +--TEST-- +Test service is called as callback for every callback type +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +callme +called +Foo::bar +array(2) { + [0]=> + string(3) "Foo" + [1]=> + string(3) "bar" +} \ No newline at end of file diff --git a/vendor/pimple/pimple/ext/pimple/tests/011.phpt b/vendor/pimple/pimple/ext/pimple/tests/011.phpt new file mode 100644 index 0000000..6682ab8 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/011.phpt @@ -0,0 +1,19 @@ +--TEST-- +Test service callback throwing an exception +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +all right! \ No newline at end of file diff --git a/vendor/pimple/pimple/ext/pimple/tests/012.phpt b/vendor/pimple/pimple/ext/pimple/tests/012.phpt new file mode 100644 index 0000000..4c6ac48 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/012.phpt @@ -0,0 +1,28 @@ +--TEST-- +Test service factory +--SKIPIF-- + +--FILE-- +factory($f = function() { var_dump('called-1'); return 'ret-1';}); + +$p[] = $f; + +$p[] = function () { var_dump('called-2'); return 'ret-2'; }; + +var_dump($p[0]); +var_dump($p[0]); +var_dump($p[1]); +var_dump($p[1]); +?> +--EXPECTF-- +string(8) "called-1" +string(5) "ret-1" +string(8) "called-1" +string(5) "ret-1" +string(8) "called-2" +string(5) "ret-2" +string(5) "ret-2" \ No newline at end of file diff --git a/vendor/pimple/pimple/ext/pimple/tests/013.phpt b/vendor/pimple/pimple/ext/pimple/tests/013.phpt new file mode 100644 index 0000000..f419958 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/013.phpt @@ -0,0 +1,33 @@ +--TEST-- +Test keys() +--SKIPIF-- + +--FILE-- +keys()); + +$p['foo'] = 'bar'; +$p[] = 'foo'; + +var_dump($p->keys()); + +unset($p['foo']); + +var_dump($p->keys()); +?> +--EXPECTF-- +array(0) { +} +array(2) { + [0]=> + string(3) "foo" + [1]=> + int(0) +} +array(1) { + [0]=> + int(0) +} \ No newline at end of file diff --git a/vendor/pimple/pimple/ext/pimple/tests/014.phpt b/vendor/pimple/pimple/ext/pimple/tests/014.phpt new file mode 100644 index 0000000..ac93721 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/014.phpt @@ -0,0 +1,30 @@ +--TEST-- +Test raw() +--SKIPIF-- + +--FILE-- +raw('foo')); +var_dump($p[42]); + +unset($p['foo']); + +try { + $p->raw('foo'); + echo "expected exception"; +} catch (InvalidArgumentException $e) { } +--EXPECTF-- +string(8) "called-2" +string(5) "ret-2" +object(Closure)#%i (0) { +} +string(8) "called-2" +string(5) "ret-2" \ No newline at end of file diff --git a/vendor/pimple/pimple/ext/pimple/tests/015.phpt b/vendor/pimple/pimple/ext/pimple/tests/015.phpt new file mode 100644 index 0000000..314f008 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/015.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test protect() +--SKIPIF-- + +--FILE-- +protect($f); + +var_dump($p['foo']); +--EXPECTF-- +object(Closure)#%i (0) { +} \ No newline at end of file diff --git a/vendor/pimple/pimple/ext/pimple/tests/016.phpt b/vendor/pimple/pimple/ext/pimple/tests/016.phpt new file mode 100644 index 0000000..e55edb0 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/016.phpt @@ -0,0 +1,24 @@ +--TEST-- +Test extend() +--SKIPIF-- + +--FILE-- +extend(12, function ($w) { var_dump($w); return 'bar'; }); /* $callable in code above */ + +var_dump($c('param')); +--EXPECTF-- +string(5) "param" +string(3) "foo" +string(3) "bar" \ No newline at end of file diff --git a/vendor/pimple/pimple/ext/pimple/tests/017.phpt b/vendor/pimple/pimple/ext/pimple/tests/017.phpt new file mode 100644 index 0000000..bac23ce --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/017.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test extend() with exception in service extension +--SKIPIF-- + +--FILE-- +extend(12, function ($w) { throw new BadMethodCallException; }); + +try { + $p[12]; + echo "Exception expected"; +} catch (BadMethodCallException $e) { } +--EXPECTF-- diff --git a/vendor/pimple/pimple/ext/pimple/tests/017_1.phpt b/vendor/pimple/pimple/ext/pimple/tests/017_1.phpt new file mode 100644 index 0000000..8f881d6 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/017_1.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test extend() with exception in service factory +--SKIPIF-- + +--FILE-- +extend(12, function ($w) { return 'foobar'; }); + +try { + $p[12]; + echo "Exception expected"; +} catch (BadMethodCallException $e) { } +--EXPECTF-- diff --git a/vendor/pimple/pimple/ext/pimple/tests/018.phpt b/vendor/pimple/pimple/ext/pimple/tests/018.phpt new file mode 100644 index 0000000..27c12a1 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/018.phpt @@ -0,0 +1,23 @@ +--TEST-- +Test register() +--SKIPIF-- + +--FILE-- +register(new Foo, array(42 => 'bar')); + +var_dump($p[42]); +--EXPECTF-- +object(Pimple\Container)#1 (0) { +} +string(3) "bar" \ No newline at end of file diff --git a/vendor/pimple/pimple/ext/pimple/tests/019.phpt b/vendor/pimple/pimple/ext/pimple/tests/019.phpt new file mode 100644 index 0000000..28a9aec --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/019.phpt @@ -0,0 +1,18 @@ +--TEST-- +Test register() returns static and is a fluent interface +--SKIPIF-- + +--FILE-- +register(new Foo)); +--EXPECTF-- +bool(true) diff --git a/vendor/pimple/pimple/ext/pimple/tests/bench.phpb b/vendor/pimple/pimple/ext/pimple/tests/bench.phpb new file mode 100644 index 0000000..8f983e6 --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/bench.phpb @@ -0,0 +1,51 @@ +factory($factory); + +$p['factory'] = $factory; + +echo $p['factory']; +echo $p['factory']; +echo $p['factory']; + +} + +echo microtime(true) - $time; diff --git a/vendor/pimple/pimple/ext/pimple/tests/bench_shared.phpb b/vendor/pimple/pimple/ext/pimple/tests/bench_shared.phpb new file mode 100644 index 0000000..aec541f --- /dev/null +++ b/vendor/pimple/pimple/ext/pimple/tests/bench_shared.phpb @@ -0,0 +1,25 @@ + diff --git a/vendor/pimple/pimple/phpunit.xml.dist b/vendor/pimple/pimple/phpunit.xml.dist new file mode 100644 index 0000000..5c8d487 --- /dev/null +++ b/vendor/pimple/pimple/phpunit.xml.dist @@ -0,0 +1,14 @@ + + + + + + ./src/Pimple/Tests + + + diff --git a/vendor/pimple/pimple/src/Pimple/Container.php b/vendor/pimple/pimple/src/Pimple/Container.php new file mode 100644 index 0000000..c976431 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Container.php @@ -0,0 +1,282 @@ +factories = new \SplObjectStorage(); + $this->protected = new \SplObjectStorage(); + + foreach ($values as $key => $value) { + $this->offsetSet($key, $value); + } + } + + /** + * Sets a parameter or an object. + * + * Objects must be defined as Closures. + * + * Allowing any PHP callable leads to difficult to debug problems + * as function names (strings) are callable (creating a function with + * the same name as an existing parameter would break your container). + * + * @param string $id The unique identifier for the parameter or object + * @param mixed $value The value of the parameter or a closure to define an object + * + * @throws \RuntimeException Prevent override of a frozen service + */ + public function offsetSet($id, $value) + { + if (isset($this->frozen[$id])) { + throw new \RuntimeException(sprintf('Cannot override frozen service "%s".', $id)); + } + + $this->values[$id] = $value; + $this->keys[$id] = true; + } + + /** + * Gets a parameter or an object. + * + * @param string $id The unique identifier for the parameter or object + * + * @return mixed The value of the parameter or an object + * + * @throws \InvalidArgumentException if the identifier is not defined + */ + public function offsetGet($id) + { + if (!isset($this->keys[$id])) { + throw new \InvalidArgumentException(sprintf('Identifier "%s" is not defined.', $id)); + } + + if ( + isset($this->raw[$id]) + || !is_object($this->values[$id]) + || isset($this->protected[$this->values[$id]]) + || !method_exists($this->values[$id], '__invoke') + ) { + return $this->values[$id]; + } + + if (isset($this->factories[$this->values[$id]])) { + return $this->values[$id]($this); + } + + $raw = $this->values[$id]; + $val = $this->values[$id] = $raw($this); + $this->raw[$id] = $raw; + + $this->frozen[$id] = true; + + return $val; + } + + /** + * Checks if a parameter or an object is set. + * + * @param string $id The unique identifier for the parameter or object + * + * @return bool + */ + public function offsetExists($id) + { + return isset($this->keys[$id]); + } + + /** + * Unsets a parameter or an object. + * + * @param string $id The unique identifier for the parameter or object + */ + public function offsetUnset($id) + { + if (isset($this->keys[$id])) { + if (is_object($this->values[$id])) { + unset($this->factories[$this->values[$id]], $this->protected[$this->values[$id]]); + } + + unset($this->values[$id], $this->frozen[$id], $this->raw[$id], $this->keys[$id]); + } + } + + /** + * Marks a callable as being a factory service. + * + * @param callable $callable A service definition to be used as a factory + * + * @return callable The passed callable + * + * @throws \InvalidArgumentException Service definition has to be a closure of an invokable object + */ + public function factory($callable) + { + if (!method_exists($callable, '__invoke')) { + throw new \InvalidArgumentException('Service definition is not a Closure or invokable object.'); + } + + $this->factories->attach($callable); + + return $callable; + } + + /** + * Protects a callable from being interpreted as a service. + * + * This is useful when you want to store a callable as a parameter. + * + * @param callable $callable A callable to protect from being evaluated + * + * @return callable The passed callable + * + * @throws \InvalidArgumentException Service definition has to be a closure of an invokable object + */ + public function protect($callable) + { + if (!method_exists($callable, '__invoke')) { + throw new \InvalidArgumentException('Callable is not a Closure or invokable object.'); + } + + $this->protected->attach($callable); + + return $callable; + } + + /** + * Gets a parameter or the closure defining an object. + * + * @param string $id The unique identifier for the parameter or object + * + * @return mixed The value of the parameter or the closure defining an object + * + * @throws \InvalidArgumentException if the identifier is not defined + */ + public function raw($id) + { + if (!isset($this->keys[$id])) { + throw new \InvalidArgumentException(sprintf('Identifier "%s" is not defined.', $id)); + } + + if (isset($this->raw[$id])) { + return $this->raw[$id]; + } + + return $this->values[$id]; + } + + /** + * Extends an object definition. + * + * Useful when you want to extend an existing object definition, + * without necessarily loading that object. + * + * @param string $id The unique identifier for the object + * @param callable $callable A service definition to extend the original + * + * @return callable The wrapped callable + * + * @throws \InvalidArgumentException if the identifier is not defined or not a service definition + */ + public function extend($id, $callable) + { + if (!isset($this->keys[$id])) { + throw new \InvalidArgumentException(sprintf('Identifier "%s" is not defined.', $id)); + } + + if (!is_object($this->values[$id]) || !method_exists($this->values[$id], '__invoke')) { + throw new \InvalidArgumentException(sprintf('Identifier "%s" does not contain an object definition.', $id)); + } + + if (!is_object($callable) || !method_exists($callable, '__invoke')) { + throw new \InvalidArgumentException('Extension service definition is not a Closure or invokable object.'); + } + + $factory = $this->values[$id]; + + $extended = function ($c) use ($callable, $factory) { + return $callable($factory($c), $c); + }; + + if (isset($this->factories[$factory])) { + $this->factories->detach($factory); + $this->factories->attach($extended); + } + + return $this[$id] = $extended; + } + + /** + * Returns all defined value names. + * + * @return array An array of value names + */ + public function keys() + { + return array_keys($this->values); + } + + /** + * Registers a service provider. + * + * @param ServiceProviderInterface $provider A ServiceProviderInterface instance + * @param array $values An array of values that customizes the provider + * + * @return static + */ + public function register(ServiceProviderInterface $provider, array $values = array()) + { + $provider->register($this); + + foreach ($values as $key => $value) { + $this[$key] = $value; + } + + return $this; + } +} diff --git a/vendor/pimple/pimple/src/Pimple/ServiceProviderInterface.php b/vendor/pimple/pimple/src/Pimple/ServiceProviderInterface.php new file mode 100644 index 0000000..c004594 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/ServiceProviderInterface.php @@ -0,0 +1,46 @@ +value = $value; + + return $service; + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/NonInvokable.php b/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/NonInvokable.php new file mode 100644 index 0000000..33cd4e5 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/NonInvokable.php @@ -0,0 +1,34 @@ +factory(function () { + return new Service(); + }); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/Service.php b/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/Service.php new file mode 100644 index 0000000..d71b184 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/Service.php @@ -0,0 +1,35 @@ + + */ +class Service +{ + public $value; +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/PimpleServiceProviderInterfaceTest.php b/vendor/pimple/pimple/src/Pimple/Tests/PimpleServiceProviderInterfaceTest.php new file mode 100644 index 0000000..8e5c4c7 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/PimpleServiceProviderInterfaceTest.php @@ -0,0 +1,76 @@ + + */ +class PimpleServiceProviderInterfaceTest extends \PHPUnit_Framework_TestCase +{ + public function testProvider() + { + $pimple = new Container(); + + $pimpleServiceProvider = new Fixtures\PimpleServiceProvider(); + $pimpleServiceProvider->register($pimple); + + $this->assertEquals('value', $pimple['param']); + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $pimple['service']); + + $serviceOne = $pimple['factory']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + + $serviceTwo = $pimple['factory']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + + $this->assertNotSame($serviceOne, $serviceTwo); + } + + public function testProviderWithRegisterMethod() + { + $pimple = new Container(); + + $pimple->register(new Fixtures\PimpleServiceProvider(), array( + 'anotherParameter' => 'anotherValue', + )); + + $this->assertEquals('value', $pimple['param']); + $this->assertEquals('anotherValue', $pimple['anotherParameter']); + + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $pimple['service']); + + $serviceOne = $pimple['factory']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + + $serviceTwo = $pimple['factory']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + + $this->assertNotSame($serviceOne, $serviceTwo); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/PimpleTest.php b/vendor/pimple/pimple/src/Pimple/Tests/PimpleTest.php new file mode 100644 index 0000000..918f620 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/PimpleTest.php @@ -0,0 +1,440 @@ + + */ +class PimpleTest extends \PHPUnit_Framework_TestCase +{ + public function testWithString() + { + $pimple = new Container(); + $pimple['param'] = 'value'; + + $this->assertEquals('value', $pimple['param']); + } + + public function testWithClosure() + { + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $pimple['service']); + } + + public function testServicesShouldBeDifferent() + { + $pimple = new Container(); + $pimple['service'] = $pimple->factory(function () { + return new Fixtures\Service(); + }); + + $serviceOne = $pimple['service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + + $serviceTwo = $pimple['service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + + $this->assertNotSame($serviceOne, $serviceTwo); + } + + public function testShouldPassContainerAsParameter() + { + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + $pimple['container'] = function ($container) { + return $container; + }; + + $this->assertNotSame($pimple, $pimple['service']); + $this->assertSame($pimple, $pimple['container']); + } + + public function testIsset() + { + $pimple = new Container(); + $pimple['param'] = 'value'; + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + + $pimple['null'] = null; + + $this->assertTrue(isset($pimple['param'])); + $this->assertTrue(isset($pimple['service'])); + $this->assertTrue(isset($pimple['null'])); + $this->assertFalse(isset($pimple['non_existent'])); + } + + public function testConstructorInjection() + { + $params = array('param' => 'value'); + $pimple = new Container($params); + + $this->assertSame($params['param'], $pimple['param']); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Identifier "foo" is not defined. + */ + public function testOffsetGetValidatesKeyIsPresent() + { + $pimple = new Container(); + echo $pimple['foo']; + } + + public function testOffsetGetHonorsNullValues() + { + $pimple = new Container(); + $pimple['foo'] = null; + $this->assertNull($pimple['foo']); + } + + public function testUnset() + { + $pimple = new Container(); + $pimple['param'] = 'value'; + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + + unset($pimple['param'], $pimple['service']); + $this->assertFalse(isset($pimple['param'])); + $this->assertFalse(isset($pimple['service'])); + } + + /** + * @dataProvider serviceDefinitionProvider + */ + public function testShare($service) + { + $pimple = new Container(); + $pimple['shared_service'] = $service; + + $serviceOne = $pimple['shared_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + + $serviceTwo = $pimple['shared_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + + $this->assertSame($serviceOne, $serviceTwo); + } + + /** + * @dataProvider serviceDefinitionProvider + */ + public function testProtect($service) + { + $pimple = new Container(); + $pimple['protected'] = $pimple->protect($service); + + $this->assertSame($service, $pimple['protected']); + } + + public function testGlobalFunctionNameAsParameterValue() + { + $pimple = new Container(); + $pimple['global_function'] = 'strlen'; + $this->assertSame('strlen', $pimple['global_function']); + } + + public function testRaw() + { + $pimple = new Container(); + $pimple['service'] = $definition = $pimple->factory(function () { return 'foo'; }); + $this->assertSame($definition, $pimple->raw('service')); + } + + public function testRawHonorsNullValues() + { + $pimple = new Container(); + $pimple['foo'] = null; + $this->assertNull($pimple->raw('foo')); + } + + public function testFluentRegister() + { + $pimple = new Container(); + $this->assertSame($pimple, $pimple->register($this->getMock('Pimple\ServiceProviderInterface'))); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Identifier "foo" is not defined. + */ + public function testRawValidatesKeyIsPresent() + { + $pimple = new Container(); + $pimple->raw('foo'); + } + + /** + * @dataProvider serviceDefinitionProvider + */ + public function testExtend($service) + { + $pimple = new Container(); + $pimple['shared_service'] = function () { + return new Fixtures\Service(); + }; + $pimple['factory_service'] = $pimple->factory(function () { + return new Fixtures\Service(); + }); + + $pimple->extend('shared_service', $service); + $serviceOne = $pimple['shared_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + $serviceTwo = $pimple['shared_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + $this->assertSame($serviceOne, $serviceTwo); + $this->assertSame($serviceOne->value, $serviceTwo->value); + + $pimple->extend('factory_service', $service); + $serviceOne = $pimple['factory_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + $serviceTwo = $pimple['factory_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + $this->assertNotSame($serviceOne, $serviceTwo); + $this->assertNotSame($serviceOne->value, $serviceTwo->value); + } + + public function testExtendDoesNotLeakWithFactories() + { + if (extension_loaded('pimple')) { + $this->markTestSkipped('Pimple extension does not support this test'); + } + $pimple = new Container(); + + $pimple['foo'] = $pimple->factory(function () { return; }); + $pimple['foo'] = $pimple->extend('foo', function ($foo, $pimple) { return; }); + unset($pimple['foo']); + + $p = new \ReflectionProperty($pimple, 'values'); + $p->setAccessible(true); + $this->assertEmpty($p->getValue($pimple)); + + $p = new \ReflectionProperty($pimple, 'factories'); + $p->setAccessible(true); + $this->assertCount(0, $p->getValue($pimple)); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Identifier "foo" is not defined. + */ + public function testExtendValidatesKeyIsPresent() + { + $pimple = new Container(); + $pimple->extend('foo', function () {}); + } + + public function testKeys() + { + $pimple = new Container(); + $pimple['foo'] = 123; + $pimple['bar'] = 123; + + $this->assertEquals(array('foo', 'bar'), $pimple->keys()); + } + + /** @test */ + public function settingAnInvokableObjectShouldTreatItAsFactory() + { + $pimple = new Container(); + $pimple['invokable'] = new Fixtures\Invokable(); + + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $pimple['invokable']); + } + + /** @test */ + public function settingNonInvokableObjectShouldTreatItAsParameter() + { + $pimple = new Container(); + $pimple['non_invokable'] = new Fixtures\NonInvokable(); + + $this->assertInstanceOf('Pimple\Tests\Fixtures\NonInvokable', $pimple['non_invokable']); + } + + /** + * @dataProvider badServiceDefinitionProvider + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Service definition is not a Closure or invokable object. + */ + public function testFactoryFailsForInvalidServiceDefinitions($service) + { + $pimple = new Container(); + $pimple->factory($service); + } + + /** + * @dataProvider badServiceDefinitionProvider + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Callable is not a Closure or invokable object. + */ + public function testProtectFailsForInvalidServiceDefinitions($service) + { + $pimple = new Container(); + $pimple->protect($service); + } + + /** + * @dataProvider badServiceDefinitionProvider + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Identifier "foo" does not contain an object definition. + */ + public function testExtendFailsForKeysNotContainingServiceDefinitions($service) + { + $pimple = new Container(); + $pimple['foo'] = $service; + $pimple->extend('foo', function () {}); + } + + /** + * @dataProvider badServiceDefinitionProvider + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Extension service definition is not a Closure or invokable object. + */ + public function testExtendFailsForInvalidServiceDefinitions($service) + { + $pimple = new Container(); + $pimple['foo'] = function () {}; + $pimple->extend('foo', $service); + } + + /** + * Provider for invalid service definitions. + */ + public function badServiceDefinitionProvider() + { + return array( + array(123), + array(new Fixtures\NonInvokable()), + ); + } + + /** + * Provider for service definitions. + */ + public function serviceDefinitionProvider() + { + return array( + array(function ($value) { + $service = new Fixtures\Service(); + $service->value = $value; + + return $service; + }), + array(new Fixtures\Invokable()), + ); + } + + public function testDefiningNewServiceAfterFreeze() + { + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $foo = $pimple['foo']; + + $pimple['bar'] = function () { + return 'bar'; + }; + $this->assertSame('bar', $pimple['bar']); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Cannot override frozen service "foo". + */ + public function testOverridingServiceAfterFreeze() + { + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $foo = $pimple['foo']; + + $pimple['foo'] = function () { + return 'bar'; + }; + } + + public function testRemovingServiceAfterFreeze() + { + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $foo = $pimple['foo']; + + unset($pimple['foo']); + $pimple['foo'] = function () { + return 'bar'; + }; + $this->assertSame('bar', $pimple['foo']); + } + + public function testExtendingService() + { + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $pimple['foo'] = $pimple->extend('foo', function ($foo, $app) { + return "$foo.bar"; + }); + $pimple['foo'] = $pimple->extend('foo', function ($foo, $app) { + return "$foo.baz"; + }); + $this->assertSame('foo.bar.baz', $pimple['foo']); + } + + public function testExtendingServiceAfterOtherServiceFreeze() + { + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $pimple['bar'] = function () { + return 'bar'; + }; + $foo = $pimple['foo']; + + $pimple['bar'] = $pimple->extend('bar', function ($bar, $app) { + return "$bar.baz"; + }); + $this->assertSame('bar.baz', $pimple['bar']); + } +} diff --git a/vendor/psr/http-message/LICENSE b/vendor/psr/http-message/LICENSE new file mode 100644 index 0000000..c2d8e45 --- /dev/null +++ b/vendor/psr/http-message/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 PHP Framework Interoperability Group + +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: + +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/vendor/psr/http-message/README.md b/vendor/psr/http-message/README.md new file mode 100644 index 0000000..2818533 --- /dev/null +++ b/vendor/psr/http-message/README.md @@ -0,0 +1,13 @@ +PSR Http Message +================ + +This repository holds all interfaces/classes/traits related to +[PSR-7](http://www.php-fig.org/psr/psr-7/). + +Note that this is not a HTTP message implementation of its own. It is merely an +interface that describes a HTTP message. See the specification for more details. + +Usage +----- + +We'll certainly need some stuff in here. \ No newline at end of file diff --git a/vendor/psr/http-message/composer.json b/vendor/psr/http-message/composer.json new file mode 100644 index 0000000..4774b61 --- /dev/null +++ b/vendor/psr/http-message/composer.json @@ -0,0 +1,25 @@ +{ + "name": "psr/http-message", + "description": "Common interface for HTTP messages", + "keywords": ["psr", "psr-7", "http", "http-message", "request", "response"], + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/vendor/psr/http-message/src/MessageInterface.php b/vendor/psr/http-message/src/MessageInterface.php new file mode 100644 index 0000000..8f67a05 --- /dev/null +++ b/vendor/psr/http-message/src/MessageInterface.php @@ -0,0 +1,187 @@ +getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return array Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings + * for that header. + */ + public function getHeaders(); + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader($name); + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader($name); + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function getHeaderLine($name); + + /** + * Return an instance with the provided value replacing the specified header. + * + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * @return self + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withHeader($name, $value); + + /** + * Return an instance with the specified header appended with the given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new header and/or value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * @return self + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withAddedHeader($name, $value); + + /** + * Return an instance without the specified header. + * + * Header resolution MUST be done without case-sensitivity. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the named header. + * + * @param string $name Case-insensitive header field name to remove. + * @return self + */ + public function withoutHeader($name); + + /** + * Gets the body of the message. + * + * @return StreamInterface Returns the body as a stream. + */ + public function getBody(); + + /** + * Return an instance with the specified message body. + * + * The body MUST be a StreamInterface object. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new body stream. + * + * @param StreamInterface $body Body. + * @return self + * @throws \InvalidArgumentException When the body is not valid. + */ + public function withBody(StreamInterface $body); +} diff --git a/vendor/psr/http-message/src/RequestInterface.php b/vendor/psr/http-message/src/RequestInterface.php new file mode 100644 index 0000000..75c802e --- /dev/null +++ b/vendor/psr/http-message/src/RequestInterface.php @@ -0,0 +1,129 @@ +getQuery()` + * or from the `QUERY_STRING` server param. + * + * @return array + */ + public function getQueryParams(); + + /** + * Return an instance with the specified query string arguments. + * + * These values SHOULD remain immutable over the course of the incoming + * request. They MAY be injected during instantiation, such as from PHP's + * $_GET superglobal, or MAY be derived from some other value such as the + * URI. In cases where the arguments are parsed from the URI, the data + * MUST be compatible with what PHP's parse_str() would return for + * purposes of how duplicate query parameters are handled, and how nested + * sets are handled. + * + * Setting query string arguments MUST NOT change the URI stored by the + * request, nor the values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated query string arguments. + * + * @param array $query Array of query string arguments, typically from + * $_GET. + * @return self + */ + public function withQueryParams(array $query); + + /** + * Retrieve normalized file upload data. + * + * This method returns upload metadata in a normalized tree, with each leaf + * an instance of Psr\Http\Message\UploadedFileInterface. + * + * These values MAY be prepared from $_FILES or the message body during + * instantiation, or MAY be injected via withUploadedFiles(). + * + * @return array An array tree of UploadedFileInterface instances; an empty + * array MUST be returned if no data is present. + */ + public function getUploadedFiles(); + + /** + * Create a new instance with the specified uploaded files. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param array An array tree of UploadedFileInterface instances. + * @return self + * @throws \InvalidArgumentException if an invalid structure is provided. + */ + public function withUploadedFiles(array $uploadedFiles); + + /** + * Retrieve any parameters provided in the request body. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, this method MUST + * return the contents of $_POST. + * + * Otherwise, this method may return any results of deserializing + * the request body content; as parsing returns structured content, the + * potential types MUST be arrays or objects only. A null value indicates + * the absence of body content. + * + * @return null|array|object The deserialized body parameters, if any. + * These will typically be an array or object. + */ + public function getParsedBody(); + + /** + * Return an instance with the specified body parameters. + * + * These MAY be injected during instantiation. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, use this method + * ONLY to inject the contents of $_POST. + * + * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of + * deserializing the request body content. Deserialization/parsing returns + * structured data, and, as such, this method ONLY accepts arrays or objects, + * or a null value if nothing was available to parse. + * + * As an example, if content negotiation determines that the request data + * is a JSON payload, this method could be used to create a request + * instance with the deserialized parameters. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param null|array|object $data The deserialized body data. This will + * typically be in an array or object. + * @return self + * @throws \InvalidArgumentException if an unsupported argument type is + * provided. + */ + public function withParsedBody($data); + + /** + * Retrieve attributes derived from the request. + * + * The request "attributes" may be used to allow injection of any + * parameters derived from the request: e.g., the results of path + * match operations; the results of decrypting cookies; the results of + * deserializing non-form-encoded message bodies; etc. Attributes + * will be application and request specific, and CAN be mutable. + * + * @return array Attributes derived from the request. + */ + public function getAttributes(); + + /** + * Retrieve a single derived request attribute. + * + * Retrieves a single derived request attribute as described in + * getAttributes(). If the attribute has not been previously set, returns + * the default value as provided. + * + * This method obviates the need for a hasAttribute() method, as it allows + * specifying a default value to return if the attribute is not found. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * @return mixed + */ + public function getAttribute($name, $default = null); + + /** + * Return an instance with the specified derived request attribute. + * + * This method allows setting a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $value The value of the attribute. + * @return self + */ + public function withAttribute($name, $value); + + /** + * Return an instance that removes the specified derived request attribute. + * + * This method allows removing a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @return self + */ + public function withoutAttribute($name); +} diff --git a/vendor/psr/http-message/src/StreamInterface.php b/vendor/psr/http-message/src/StreamInterface.php new file mode 100644 index 0000000..f68f391 --- /dev/null +++ b/vendor/psr/http-message/src/StreamInterface.php @@ -0,0 +1,158 @@ + + * [user-info@]host[:port] + * + * + * If the port component is not set or is the standard port for the current + * scheme, it SHOULD NOT be included. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2 + * @return string The URI authority, in "[user-info@]host[:port]" format. + */ + public function getAuthority(); + + /** + * Retrieve the user information component of the URI. + * + * If no user information is present, this method MUST return an empty + * string. + * + * If a user is present in the URI, this will return that value; + * additionally, if the password is also present, it will be appended to the + * user value, with a colon (":") separating the values. + * + * The trailing "@" character is not part of the user information and MUST + * NOT be added. + * + * @return string The URI user information, in "username[:password]" format. + */ + public function getUserInfo(); + + /** + * Retrieve the host component of the URI. + * + * If no host is present, this method MUST return an empty string. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.2.2. + * + * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 + * @return string The URI host. + */ + public function getHost(); + + /** + * Retrieve the port component of the URI. + * + * If a port is present, and it is non-standard for the current scheme, + * this method MUST return it as an integer. If the port is the standard port + * used with the current scheme, this method SHOULD return null. + * + * If no port is present, and no scheme is present, this method MUST return + * a null value. + * + * If no port is present, but a scheme is present, this method MAY return + * the standard port for that scheme, but SHOULD return null. + * + * @return null|int The URI port. + */ + public function getPort(); + + /** + * Retrieve the path component of the URI. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * Normally, the empty path "" and absolute path "/" are considered equal as + * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically + * do this normalization because in contexts with a trimmed base path, e.g. + * the front controller, this difference becomes significant. It's the task + * of the user to handle both "" and "/". + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.3. + * + * As an example, if the value should include a slash ("/") not intended as + * delimiter between path segments, that value MUST be passed in encoded + * form (e.g., "%2F") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.3 + * @return string The URI path. + */ + public function getPath(); + + /** + * Retrieve the query string of the URI. + * + * If no query string is present, this method MUST return an empty string. + * + * The leading "?" character is not part of the query and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.4. + * + * As an example, if a value in a key/value pair of the query string should + * include an ampersand ("&") not intended as a delimiter between values, + * that value MUST be passed in encoded form (e.g., "%26") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.4 + * @return string The URI query string. + */ + public function getQuery(); + + /** + * Retrieve the fragment component of the URI. + * + * If no fragment is present, this method MUST return an empty string. + * + * The leading "#" character is not part of the fragment and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.5. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.5 + * @return string The URI fragment. + */ + public function getFragment(); + + /** + * Return an instance with the specified scheme. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified scheme. + * + * Implementations MUST support the schemes "http" and "https" case + * insensitively, and MAY accommodate other schemes if required. + * + * An empty scheme is equivalent to removing the scheme. + * + * @param string $scheme The scheme to use with the new instance. + * @return self A new instance with the specified scheme. + * @throws \InvalidArgumentException for invalid or unsupported schemes. + */ + public function withScheme($scheme); + + /** + * Return an instance with the specified user information. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified user information. + * + * Password is optional, but the user information MUST include the + * user; an empty string for the user is equivalent to removing user + * information. + * + * @param string $user The user name to use for authority. + * @param null|string $password The password associated with $user. + * @return self A new instance with the specified user information. + */ + public function withUserInfo($user, $password = null); + + /** + * Return an instance with the specified host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified host. + * + * An empty host value is equivalent to removing the host. + * + * @param string $host The hostname to use with the new instance. + * @return self A new instance with the specified host. + * @throws \InvalidArgumentException for invalid hostnames. + */ + public function withHost($host); + + /** + * Return an instance with the specified port. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified port. + * + * Implementations MUST raise an exception for ports outside the + * established TCP and UDP port ranges. + * + * A null value provided for the port is equivalent to removing the port + * information. + * + * @param null|int $port The port to use with the new instance; a null value + * removes the port information. + * @return self A new instance with the specified port. + * @throws \InvalidArgumentException for invalid ports. + */ + public function withPort($port); + + /** + * Return an instance with the specified path. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified path. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * If the path is intended to be domain-relative rather than path relative then + * it must begin with a slash ("/"). Paths not starting with a slash ("/") + * are assumed to be relative to some base path known to the application or + * consumer. + * + * Users can provide both encoded and decoded path characters. + * Implementations ensure the correct encoding as outlined in getPath(). + * + * @param string $path The path to use with the new instance. + * @return self A new instance with the specified path. + * @throws \InvalidArgumentException for invalid paths. + */ + public function withPath($path); + + /** + * Return an instance with the specified query string. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified query string. + * + * Users can provide both encoded and decoded query characters. + * Implementations ensure the correct encoding as outlined in getQuery(). + * + * An empty query string value is equivalent to removing the query string. + * + * @param string $query The query string to use with the new instance. + * @return self A new instance with the specified query string. + * @throws \InvalidArgumentException for invalid query strings. + */ + public function withQuery($query); + + /** + * Return an instance with the specified URI fragment. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified URI fragment. + * + * Users can provide both encoded and decoded fragment characters. + * Implementations ensure the correct encoding as outlined in getFragment(). + * + * An empty fragment value is equivalent to removing the fragment. + * + * @param string $fragment The fragment to use with the new instance. + * @return self A new instance with the specified fragment. + */ + public function withFragment($fragment); + + /** + * Return the string representation as a URI reference. + * + * Depending on which components of the URI are present, the resulting + * string is either a full URI or relative reference according to RFC 3986, + * Section 4.1. The method concatenates the various components of the URI, + * using the appropriate delimiters: + * + * - If a scheme is present, it MUST be suffixed by ":". + * - If an authority is present, it MUST be prefixed by "//". + * - The path can be concatenated without delimiters. But there are two + * cases where the path has to be adjusted to make the URI reference + * valid as PHP does not allow to throw an exception in __toString(): + * - If the path is rootless and an authority is present, the path MUST + * be prefixed by "/". + * - If the path is starting with more than one "/" and no authority is + * present, the starting slashes MUST be reduced to one. + * - If a query is present, it MUST be prefixed by "?". + * - If a fragment is present, it MUST be prefixed by "#". + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + * @return string + */ + public function __toString(); +} diff --git a/vendor/slim/slim/CONTRIBUTING.md b/vendor/slim/slim/CONTRIBUTING.md new file mode 100644 index 0000000..9bbb6b1 --- /dev/null +++ b/vendor/slim/slim/CONTRIBUTING.md @@ -0,0 +1,20 @@ +# How to Contribute + +## Pull Requests + +1. Fork the Slim Framework repository +2. Create a new branch for each feature or improvement +3. Send a pull request from each feature branch to the **develop** branch + +It is very important to separate new features or improvements into separate feature branches, and to send a +pull request for each branch. This allows me to review and pull in new features or improvements individually. + +## Style Guide + +All pull requests must adhere to the [PSR-2 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md). + +## Unit Testing + +All pull requests must be accompanied by passing unit tests and complete code coverage. The Slim Framework uses phpunit for testing. + +[Learn about PHPUnit](https://github.com/sebastianbergmann/phpunit/) diff --git a/vendor/slim/slim/LICENSE.md b/vendor/slim/slim/LICENSE.md new file mode 100644 index 0000000..0875f84 --- /dev/null +++ b/vendor/slim/slim/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) 2011-2016 Josh Lockhart + +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: + +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/vendor/slim/slim/README.md b/vendor/slim/slim/README.md new file mode 100644 index 0000000..2333614 --- /dev/null +++ b/vendor/slim/slim/README.md @@ -0,0 +1,84 @@ +# Slim Framework + +[![Build Status](https://travis-ci.org/slimphp/Slim.svg?branch=develop)](https://travis-ci.org/slimphp/Slim) +[![Coverage Status](https://coveralls.io/repos/slimphp/Slim/badge.svg?branch=develop)](https://coveralls.io/r/slimphp/Slim?branch=develop) +[![Total Downloads](https://poser.pugx.org/slim/slim/downloads)](https://packagist.org/packages/slim/slim) +[![License](https://poser.pugx.org/slim/slim/license)](https://packagist.org/packages/slim/slim) + +Slim is a PHP micro-framework that helps you quickly write simple yet powerful web applications and APIs. + +## Installation + +It's recommended that you use [Composer](https://getcomposer.org/) to install Slim. + +```bash +$ composer require slim/slim "^3.0" +``` + +This will install Slim and all required dependencies. Slim requires PHP 5.5.0 or newer. + +## Usage + +Create an index.php file with the following contents: + +```php +get('/hello/{name}', function ($request, $response, $args) { + $response->write("Hello, " . $args['name']); + return $response; +}); + +$app->run(); +``` + +You may quickly test this using the built-in PHP server: +```bash +$ php -S localhost:8000 +``` + +Going to http://localhost:8000/hello/world will now display "Hello, world". + +For more information on how to configure your web server, see the [Documentation](http://www.slimframework.com/docs/start/web-servers.html). + +## Tests + +To execute the test suite, you'll need phpunit. + +```bash +$ phpunit +``` + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +## Learn More + +Learn more at these links: + +- [Website](http://www.slimframework.com) +- [Documentation](http://www.slimframework.com/docs/start/installation.html) +- [Support Forum](http://help.slimframework.com) +- [Twitter](https://twitter.com/slimphp) +- [Resources](https://github.com/xssc/awesome-slim) + +## Security + +If you discover security related issues, please email security@slimframework.com instead of using the issue tracker. + +## Credits + +- [Josh Lockhart](https://github.com/codeguy) +- [Andrew Smith](https://github.com/silentworks) +- [Rob Allen](https://github.com/akrabat) +- [Gabriel Manricks](https://github.com/gmanricks) +- [All Contributors](../../contributors) + +## License + +The Slim Framework is licensed under the MIT license. See [License File](LICENSE.md) for more information. diff --git a/vendor/slim/slim/Slim/App.php b/vendor/slim/slim/Slim/App.php new file mode 100644 index 0000000..0bfac94 --- /dev/null +++ b/vendor/slim/slim/Slim/App.php @@ -0,0 +1,635 @@ +container = $container; + } + + /** + * Enable access to the DI container by consumers of $app + * + * @return ContainerInterface + */ + public function getContainer() + { + return $this->container; + } + + /** + * Add middleware + * + * This method prepends new middleware to the app's middleware stack. + * + * @param callable|string $callable The callback routine + * + * @return static + */ + public function add($callable) + { + return $this->addMiddleware(new DeferredCallable($callable, $this->container)); + } + + /** + * Calling a non-existant method on App checks to see if there's an item + * in the container than is callable and if so, calls it. + * + * @param string $method + * @param array $args + * @return mixed + */ + public function __call($method, $args) + { + if ($this->container->has($method)) { + $obj = $this->container->get($method); + if (is_callable($obj)) { + return call_user_func_array($obj, $args); + } + } + + throw new \BadMethodCallException("Method $method is not a valid method"); + } + + /******************************************************************************** + * Router proxy methods + *******************************************************************************/ + + /** + * Add GET route + * + * @param string $pattern The route URI pattern + * @param callable|string $callable The route callback routine + * + * @return \Slim\Interfaces\RouteInterface + */ + public function get($pattern, $callable) + { + return $this->map(['GET'], $pattern, $callable); + } + + /** + * Add POST route + * + * @param string $pattern The route URI pattern + * @param callable|string $callable The route callback routine + * + * @return \Slim\Interfaces\RouteInterface + */ + public function post($pattern, $callable) + { + return $this->map(['POST'], $pattern, $callable); + } + + /** + * Add PUT route + * + * @param string $pattern The route URI pattern + * @param callable|string $callable The route callback routine + * + * @return \Slim\Interfaces\RouteInterface + */ + public function put($pattern, $callable) + { + return $this->map(['PUT'], $pattern, $callable); + } + + /** + * Add PATCH route + * + * @param string $pattern The route URI pattern + * @param callable|string $callable The route callback routine + * + * @return \Slim\Interfaces\RouteInterface + */ + public function patch($pattern, $callable) + { + return $this->map(['PATCH'], $pattern, $callable); + } + + /** + * Add DELETE route + * + * @param string $pattern The route URI pattern + * @param callable|string $callable The route callback routine + * + * @return \Slim\Interfaces\RouteInterface + */ + public function delete($pattern, $callable) + { + return $this->map(['DELETE'], $pattern, $callable); + } + + /** + * Add OPTIONS route + * + * @param string $pattern The route URI pattern + * @param callable|string $callable The route callback routine + * + * @return \Slim\Interfaces\RouteInterface + */ + public function options($pattern, $callable) + { + return $this->map(['OPTIONS'], $pattern, $callable); + } + + /** + * Add route for any HTTP method + * + * @param string $pattern The route URI pattern + * @param callable|string $callable The route callback routine + * + * @return \Slim\Interfaces\RouteInterface + */ + public function any($pattern, $callable) + { + return $this->map(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], $pattern, $callable); + } + + /** + * Add route with multiple methods + * + * @param string[] $methods Numeric array of HTTP method names + * @param string $pattern The route URI pattern + * @param callable|string $callable The route callback routine + * + * @return RouteInterface + */ + public function map(array $methods, $pattern, $callable) + { + if ($callable instanceof Closure) { + $callable = $callable->bindTo($this->container); + } + + $route = $this->container->get('router')->map($methods, $pattern, $callable); + if (is_callable([$route, 'setContainer'])) { + $route->setContainer($this->container); + } + + if (is_callable([$route, 'setOutputBuffering'])) { + $route->setOutputBuffering($this->container->get('settings')['outputBuffering']); + } + + return $route; + } + + /** + * Route Groups + * + * This method accepts a route pattern and a callback. All route + * declarations in the callback will be prepended by the group(s) + * that it is in. + * + * @param string $pattern + * @param callable $callable + * + * @return RouteGroupInterface + */ + public function group($pattern, $callable) + { + /** @var RouteGroup $group */ + $group = $this->container->get('router')->pushGroup($pattern, $callable); + $group->setContainer($this->container); + $group($this); + $this->container->get('router')->popGroup(); + return $group; + } + + /******************************************************************************** + * Runner + *******************************************************************************/ + + /** + * Run application + * + * This method traverses the application middleware stack and then sends the + * resultant Response object to the HTTP client. + * + * @param bool|false $silent + * @return ResponseInterface + * + * @throws Exception + * @throws MethodNotAllowedException + * @throws NotFoundException + */ + public function run($silent = false) + { + $request = $this->container->get('request'); + $response = $this->container->get('response'); + + $response = $this->process($request, $response); + + if (!$silent) { + $this->respond($response); + } + + return $response; + } + + /** + * Process a request + * + * This method traverses the application middleware stack and then returns the + * resultant Response object. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return ResponseInterface + * + * @throws Exception + * @throws MethodNotAllowedException + * @throws NotFoundException + */ + public function process(ServerRequestInterface $request, ResponseInterface $response) + { + // Ensure basePath is set + $router = $this->container->get('router'); + if (is_callable([$request->getUri(), 'getBasePath']) && is_callable([$router, 'setBasePath'])) { + $router->setBasePath($request->getUri()->getBasePath()); + } + + // Dispatch the Router first if the setting for this is on + if ($this->container->get('settings')['determineRouteBeforeAppMiddleware'] === true) { + // Dispatch router (note: you won't be able to alter routes after this) + $request = $this->dispatchRouterAndPrepareRoute($request, $router); + } + + // Traverse middleware stack + try { + $response = $this->callMiddlewareStack($request, $response); + } catch (Exception $e) { + $response = $this->handleException($e, $request, $response); + } catch (Throwable $e) { + $response = $this->handlePhpError($e, $request, $response); + } + + $response = $this->finalize($response); + + return $response; + } + + /** + * Send the response the client + * + * @param ResponseInterface $response + */ + public function respond(ResponseInterface $response) + { + // Send response + if (!headers_sent()) { + // Status + header(sprintf( + 'HTTP/%s %s %s', + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() + )); + + // Headers + foreach ($response->getHeaders() as $name => $values) { + foreach ($values as $value) { + header(sprintf('%s: %s', $name, $value), false); + } + } + } + + // Body + if (!$this->isEmptyResponse($response)) { + $body = $response->getBody(); + if ($body->isSeekable()) { + $body->rewind(); + } + $settings = $this->container->get('settings'); + $chunkSize = $settings['responseChunkSize']; + $contentLength = $response->getHeaderLine('Content-Length'); + if (!$contentLength) { + $contentLength = $body->getSize(); + } + if (isset($contentLength)) { + $totalChunks = ceil($contentLength / $chunkSize); + $lastChunkSize = $contentLength % $chunkSize; + $currentChunk = 0; + while (!$body->eof() && $currentChunk < $totalChunks) { + if (++$currentChunk == $totalChunks && $lastChunkSize > 0) { + $chunkSize = $lastChunkSize; + } + echo $body->read($chunkSize); + if (connection_status() != CONNECTION_NORMAL) { + break; + } + } + } else { + while (!$body->eof()) { + echo $body->read($chunkSize); + if (connection_status() != CONNECTION_NORMAL) { + break; + } + } + } + } + } + + /** + * Invoke application + * + * This method implements the middleware interface. It receives + * Request and Response objects, and it returns a Response object + * after compiling the routes registered in the Router and dispatching + * the Request object to the appropriate Route callback routine. + * + * @param ServerRequestInterface $request The most recent Request object + * @param ResponseInterface $response The most recent Response object + * + * @return ResponseInterface + * @throws MethodNotAllowedException + * @throws NotFoundException + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response) + { + // Get the route info + $routeInfo = $request->getAttribute('routeInfo'); + + /** @var \Slim\Interfaces\RouterInterface $router */ + $router = $this->container->get('router'); + + // If router hasn't been dispatched or the URI changed then dispatch + if (null === $routeInfo || ($routeInfo['request'] !== [$request->getMethod(), (string) $request->getUri()])) { + $request = $this->dispatchRouterAndPrepareRoute($request, $router); + $routeInfo = $request->getAttribute('routeInfo'); + } + + if ($routeInfo[0] === Dispatcher::FOUND) { + $route = $router->lookupRoute($routeInfo[1]); + return $route->run($request, $response); + } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) { + if (!$this->container->has('notAllowedHandler')) { + throw new MethodNotAllowedException($request, $response, $routeInfo[1]); + } + /** @var callable $notAllowedHandler */ + $notAllowedHandler = $this->container->get('notAllowedHandler'); + return $notAllowedHandler($request, $response, $routeInfo[1]); + } + + if (!$this->container->has('notFoundHandler')) { + throw new NotFoundException($request, $response); + } + /** @var callable $notFoundHandler */ + $notFoundHandler = $this->container->get('notFoundHandler'); + return $notFoundHandler($request, $response); + } + + /** + * Perform a sub-request from within an application route + * + * This method allows you to prepare and initiate a sub-request, run within + * the context of the current request. This WILL NOT issue a remote HTTP + * request. Instead, it will route the provided URL, method, headers, + * cookies, body, and server variables against the set of registered + * application routes. The result response object is returned. + * + * @param string $method The request method (e.g., GET, POST, PUT, etc.) + * @param string $path The request URI path + * @param string $query The request URI query string + * @param array $headers The request headers (key-value array) + * @param array $cookies The request cookies (key-value array) + * @param string $bodyContent The request body + * @param ResponseInterface $response The response object (optional) + * @return ResponseInterface + */ + public function subRequest( + $method, + $path, + $query = '', + array $headers = [], + array $cookies = [], + $bodyContent = '', + ResponseInterface $response = null + ) { + $env = $this->container->get('environment'); + $uri = Uri::createFromEnvironment($env)->withPath($path)->withQuery($query); + $headers = new Headers($headers); + $serverParams = $env->all(); + $body = new Body(fopen('php://temp', 'r+')); + $body->write($bodyContent); + $body->rewind(); + $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body); + + if (!$response) { + $response = $this->container->get('response'); + } + + return $this($request, $response); + } + + /** + * Dispatch the router to find the route. Prepare the route for use. + * + * @param ServerRequestInterface $request + * @param RouterInterface $router + * @return ServerRequestInterface + */ + protected function dispatchRouterAndPrepareRoute(ServerRequestInterface $request, RouterInterface $router) + { + $routeInfo = $router->dispatch($request); + + if ($routeInfo[0] === Dispatcher::FOUND) { + $routeArguments = []; + foreach ($routeInfo[2] as $k => $v) { + $routeArguments[$k] = urldecode($v); + } + + $route = $router->lookupRoute($routeInfo[1]); + $route->prepare($request, $routeArguments); + + // add route to the request's attributes in case a middleware or handler needs access to the route + $request = $request->withAttribute('route', $route); + } + + $routeInfo['request'] = [$request->getMethod(), (string) $request->getUri()]; + + return $request->withAttribute('routeInfo', $routeInfo); + } + + /** + * Finalize response + * + * @param ResponseInterface $response + * @return ResponseInterface + */ + protected function finalize(ResponseInterface $response) + { + // stop PHP sending a Content-Type automatically + ini_set('default_mimetype', ''); + + if ($this->isEmptyResponse($response)) { + return $response->withoutHeader('Content-Type')->withoutHeader('Content-Length'); + } + + $size = $response->getBody()->getSize(); + if ($size !== null && !$response->hasHeader('Content-Length')) { + $response = $response->withHeader('Content-Length', (string) $size); + } + + return $response; + } + + /** + * Helper method, which returns true if the provided response must not output a body and false + * if the response could have a body. + * + * @see https://tools.ietf.org/html/rfc7231 + * + * @param ResponseInterface $response + * @return bool + */ + protected function isEmptyResponse(ResponseInterface $response) + { + if (method_exists($response, 'isEmpty')) { + return $response->isEmpty(); + } + + return in_array($response->getStatusCode(), [204, 205, 304]); + } + + /** + * Call relevant handler from the Container if needed. If it doesn't exist, + * then just re-throw. + * + * @param Exception $e + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * + * @return ResponseInterface + * @throws Exception if a handler is needed and not found + */ + protected function handleException(Exception $e, ServerRequestInterface $request, ResponseInterface $response) + { + if ($e instanceof MethodNotAllowedException) { + $handler = 'notAllowedHandler'; + $params = [$e->getRequest(), $e->getResponse(), $e->getAllowedMethods()]; + } elseif ($e instanceof NotFoundException) { + $handler = 'notFoundHandler'; + $params = [$e->getRequest(), $e->getResponse()]; + } elseif ($e instanceof SlimException) { + // This is a Stop exception and contains the response + return $e->getResponse(); + } else { + // Other exception, use $request and $response params + $handler = 'errorHandler'; + $params = [$request, $response, $e]; + } + + if ($this->container->has($handler)) { + $callable = $this->container->get($handler); + // Call the registered handler + return call_user_func_array($callable, $params); + } + + // No handlers found, so just throw the exception + throw $e; + } + + /** + * Call relevant handler from the Container if needed. If it doesn't exist, + * then just re-throw. + * + * @param Throwable $e + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * + * @return ResponseInterface + * @throws Exception if a handler is needed and not found + */ + protected function handlePhpError(Throwable $e, ServerRequestInterface $request, ResponseInterface $response) + { + $handler = 'phpErrorHandler'; + $params = [$request, $response, $e]; + + if ($this->container->has($handler)) { + $callable = $this->container->get($handler); + // Call the registered handler + return call_user_func_array($callable, $params); + } + + // No handlers found, so just throw the exception + throw $e; + } +} diff --git a/vendor/slim/slim/Slim/CallableResolver.php b/vendor/slim/slim/Slim/CallableResolver.php new file mode 100644 index 0000000..705a9f2 --- /dev/null +++ b/vendor/slim/slim/Slim/CallableResolver.php @@ -0,0 +1,87 @@ +container = $container; + } + + /** + * Resolve toResolve into a closure that that the router can dispatch. + * + * If toResolve is of the format 'class:method', then try to extract 'class' + * from the container otherwise instantiate it and then dispatch 'method'. + * + * @param mixed $toResolve + * + * @return callable + * + * @throws RuntimeException if the callable does not exist + * @throws RuntimeException if the callable is not resolvable + */ + public function resolve($toResolve) + { + $resolved = $toResolve; + + if (!is_callable($toResolve) && is_string($toResolve)) { + // check for slim callable as "class:method" + $callablePattern = '!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!'; + if (preg_match($callablePattern, $toResolve, $matches)) { + $class = $matches[1]; + $method = $matches[2]; + + if ($this->container->has($class)) { + $resolved = [$this->container->get($class), $method]; + } else { + if (!class_exists($class)) { + throw new RuntimeException(sprintf('Callable %s does not exist', $class)); + } + $resolved = [new $class($this->container), $method]; + } + } else { + // check if string is something in the DIC that's callable or is a class name which + // has an __invoke() method + $class = $toResolve; + if ($this->container->has($class)) { + $resolved = $this->container->get($class); + } else { + if (!class_exists($class)) { + throw new RuntimeException(sprintf('Callable %s does not exist', $class)); + } + $resolved = new $class($this->container); + } + } + } + + if (!is_callable($resolved)) { + throw new RuntimeException(sprintf('%s is not resolvable', $toResolve)); + } + + return $resolved; + } +} diff --git a/vendor/slim/slim/Slim/CallableResolverAwareTrait.php b/vendor/slim/slim/Slim/CallableResolverAwareTrait.php new file mode 100644 index 0000000..f7ff485 --- /dev/null +++ b/vendor/slim/slim/Slim/CallableResolverAwareTrait.php @@ -0,0 +1,47 @@ +container instanceof ContainerInterface) { + return $callable; + } + + /** @var CallableResolverInterface $resolver */ + $resolver = $this->container->get('callableResolver'); + + return $resolver->resolve($callable); + } +} diff --git a/vendor/slim/slim/Slim/Collection.php b/vendor/slim/slim/Slim/Collection.php new file mode 100644 index 0000000..d33acd9 --- /dev/null +++ b/vendor/slim/slim/Slim/Collection.php @@ -0,0 +1,204 @@ + $value) { + $this->set($key, $value); + } + } + + /******************************************************************************** + * Collection interface + *******************************************************************************/ + + /** + * Set collection item + * + * @param string $key The data key + * @param mixed $value The data value + */ + public function set($key, $value) + { + $this->data[$key] = $value; + } + + /** + * Get collection item for key + * + * @param string $key The data key + * @param mixed $default The default value to return if data key does not exist + * + * @return mixed The key's value, or the default value + */ + public function get($key, $default = null) + { + return $this->has($key) ? $this->data[$key] : $default; + } + + /** + * Add item to collection + * + * @param array $items Key-value array of data to append to this collection + */ + public function replace(array $items) + { + foreach ($items as $key => $value) { + $this->set($key, $value); + } + } + + /** + * Get all items in collection + * + * @return array The collection's source data + */ + public function all() + { + return $this->data; + } + + /** + * Get collection keys + * + * @return array The collection's source data keys + */ + public function keys() + { + return array_keys($this->data); + } + + /** + * Does this collection have a given key? + * + * @param string $key The data key + * + * @return bool + */ + public function has($key) + { + return array_key_exists($key, $this->data); + } + + /** + * Remove item from collection + * + * @param string $key The data key + */ + public function remove($key) + { + unset($this->data[$key]); + } + + /** + * Remove all items from collection + */ + public function clear() + { + $this->data = []; + } + + /******************************************************************************** + * ArrayAccess interface + *******************************************************************************/ + + /** + * Does this collection have a given key? + * + * @param string $key The data key + * + * @return bool + */ + public function offsetExists($key) + { + return $this->has($key); + } + + /** + * Get collection item for key + * + * @param string $key The data key + * + * @return mixed The key's value, or the default value + */ + public function offsetGet($key) + { + return $this->get($key); + } + + /** + * Set collection item + * + * @param string $key The data key + * @param mixed $value The data value + */ + public function offsetSet($key, $value) + { + $this->set($key, $value); + } + + /** + * Remove item from collection + * + * @param string $key The data key + */ + public function offsetUnset($key) + { + $this->remove($key); + } + + /** + * Get number of items in collection + * + * @return int + */ + public function count() + { + return count($this->data); + } + + /******************************************************************************** + * IteratorAggregate interface + *******************************************************************************/ + + /** + * Get collection iterator + * + * @return \ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->data); + } +} diff --git a/vendor/slim/slim/Slim/Container.php b/vendor/slim/slim/Slim/Container.php new file mode 100644 index 0000000..ff95b28 --- /dev/null +++ b/vendor/slim/slim/Slim/Container.php @@ -0,0 +1,151 @@ + '1.1', + 'responseChunkSize' => 4096, + 'outputBuffering' => 'append', + 'determineRouteBeforeAppMiddleware' => false, + 'displayErrorDetails' => false, + ]; + + /** + * Create new container + * + * @param array $values The parameters or objects. + */ + public function __construct(array $values = []) + { + parent::__construct($values); + + $userSettings = isset($values['settings']) ? $values['settings'] : []; + $this->registerDefaultServices($userSettings); + } + + /** + * This function registers the default services that Slim needs to work. + * + * All services are shared - that is, they are registered such that the + * same instance is returned on subsequent calls. + * + * @param array $userSettings Associative array of application settings + * + * @return void + */ + private function registerDefaultServices($userSettings) + { + $defaultSettings = $this->defaultSettings; + + /** + * This service MUST return an array or an + * instance of \ArrayAccess. + * + * @return array|\ArrayAccess + */ + $this['settings'] = function () use ($userSettings, $defaultSettings) { + return new Collection(array_merge($defaultSettings, $userSettings)); + }; + + $defaultProvider = new DefaultServicesProvider(); + $defaultProvider->register($this); + } + + /******************************************************************************** + * Methods to satisfy Interop\Container\ContainerInterface + *******************************************************************************/ + + /** + * Finds an entry of the container by its identifier and returns it. + * + * @param string $id Identifier of the entry to look for. + * + * @throws ContainerValueNotFoundException No entry was found for this identifier. + * @throws ContainerException Error while retrieving the entry. + * + * @return mixed Entry. + */ + public function get($id) + { + if (!$this->offsetExists($id)) { + throw new ContainerValueNotFoundException(sprintf('Identifier "%s" is not defined.', $id)); + } + return $this->offsetGet($id); + } + + /** + * Returns true if the container can return an entry for the given identifier. + * Returns false otherwise. + * + * @param string $id Identifier of the entry to look for. + * + * @return boolean + */ + public function has($id) + { + return $this->offsetExists($id); + } + + + /******************************************************************************** + * Magic methods for convenience + *******************************************************************************/ + + public function __get($name) + { + return $this->get($name); + } + + public function __isset($name) + { + return $this->has($name); + } +} diff --git a/vendor/slim/slim/Slim/DefaultServicesProvider.php b/vendor/slim/slim/Slim/DefaultServicesProvider.php new file mode 100644 index 0000000..e41831b --- /dev/null +++ b/vendor/slim/slim/Slim/DefaultServicesProvider.php @@ -0,0 +1,197 @@ +get('environment')); + }; + } + + if (!isset($container['response'])) { + /** + * PSR-7 Response object + * + * @param Container $container + * + * @return ResponseInterface + */ + $container['response'] = function ($container) { + $headers = new Headers(['Content-Type' => 'text/html; charset=UTF-8']); + $response = new Response(200, $headers); + + return $response->withProtocolVersion($container->get('settings')['httpVersion']); + }; + } + + if (!isset($container['router'])) { + /** + * This service MUST return a SHARED instance + * of \Slim\Interfaces\RouterInterface. + * + * @return RouterInterface + */ + $container['router'] = function () { + return new Router; + }; + } + + if (!isset($container['foundHandler'])) { + /** + * This service MUST return a SHARED instance + * of \Slim\Interfaces\InvocationStrategyInterface. + * + * @return InvocationStrategyInterface + */ + $container['foundHandler'] = function () { + return new RequestResponse; + }; + } + + if (!isset($container['phpErrorHandler'])) { + /** + * This service MUST return a callable + * that accepts three arguments: + * + * 1. Instance of \Psr\Http\Message\ServerRequestInterface + * 2. Instance of \Psr\Http\Message\ResponseInterface + * 3. Instance of \Error + * + * The callable MUST return an instance of + * \Psr\Http\Message\ResponseInterface. + * + * @param Container $container + * + * @return callable + */ + $container['phpErrorHandler'] = function ($container) { + return new PhpError($container->get('settings')['displayErrorDetails']); + }; + } + + if (!isset($container['errorHandler'])) { + /** + * This service MUST return a callable + * that accepts three arguments: + * + * 1. Instance of \Psr\Http\Message\ServerRequestInterface + * 2. Instance of \Psr\Http\Message\ResponseInterface + * 3. Instance of \Exception + * + * The callable MUST return an instance of + * \Psr\Http\Message\ResponseInterface. + * + * @param Container $container + * + * @return callable + */ + $container['errorHandler'] = function ($container) { + return new Error($container->get('settings')['displayErrorDetails']); + }; + } + + if (!isset($container['notFoundHandler'])) { + /** + * This service MUST return a callable + * that accepts two arguments: + * + * 1. Instance of \Psr\Http\Message\ServerRequestInterface + * 2. Instance of \Psr\Http\Message\ResponseInterface + * + * The callable MUST return an instance of + * \Psr\Http\Message\ResponseInterface. + * + * @return callable + */ + $container['notFoundHandler'] = function () { + return new NotFound; + }; + } + + if (!isset($container['notAllowedHandler'])) { + /** + * This service MUST return a callable + * that accepts three arguments: + * + * 1. Instance of \Psr\Http\Message\ServerRequestInterface + * 2. Instance of \Psr\Http\Message\ResponseInterface + * 3. Array of allowed HTTP methods + * + * The callable MUST return an instance of + * \Psr\Http\Message\ResponseInterface. + * + * @return callable + */ + $container['notAllowedHandler'] = function () { + return new NotAllowed; + }; + } + + if (!isset($container['callableResolver'])) { + /** + * Instance of \Slim\Interfaces\CallableResolverInterface + * + * @param Container $container + * + * @return CallableResolverInterface + */ + $container['callableResolver'] = function ($container) { + return new CallableResolver($container); + }; + } + } +} diff --git a/vendor/slim/slim/Slim/DeferredCallable.php b/vendor/slim/slim/Slim/DeferredCallable.php new file mode 100644 index 0000000..def58ab --- /dev/null +++ b/vendor/slim/slim/Slim/DeferredCallable.php @@ -0,0 +1,39 @@ +callable = $callable; + $this->container = $container; + } + + public function __invoke() + { + $callable = $this->resolveCallable($this->callable); + if ($callable instanceof Closure) { + $callable = $callable->bindTo($this->container); + } + + $args = func_get_args(); + + return call_user_func_array($callable, $args); + } +} diff --git a/vendor/slim/slim/Slim/Exception/ContainerValueNotFoundException.php b/vendor/slim/slim/Slim/Exception/ContainerValueNotFoundException.php new file mode 100644 index 0000000..0ea7671 --- /dev/null +++ b/vendor/slim/slim/Slim/Exception/ContainerValueNotFoundException.php @@ -0,0 +1,20 @@ +allowedMethods = $allowedMethods; + } + + /** + * Get allowed methods + * + * @return string[] + */ + public function getAllowedMethods() + { + return $this->allowedMethods; + } +} diff --git a/vendor/slim/slim/Slim/Exception/NotFoundException.php b/vendor/slim/slim/Slim/Exception/NotFoundException.php new file mode 100644 index 0000000..65365eb --- /dev/null +++ b/vendor/slim/slim/Slim/Exception/NotFoundException.php @@ -0,0 +1,14 @@ +request = $request; + $this->response = $response; + } + + /** + * Get request + * + * @return ServerRequestInterface + */ + public function getRequest() + { + return $this->request; + } + + /** + * Get response + * + * @return ResponseInterface + */ + public function getResponse() + { + return $this->response; + } +} diff --git a/vendor/slim/slim/Slim/Handlers/Error.php b/vendor/slim/slim/Slim/Handlers/Error.php new file mode 100644 index 0000000..c34e3c8 --- /dev/null +++ b/vendor/slim/slim/Slim/Handlers/Error.php @@ -0,0 +1,300 @@ +displayErrorDetails = (bool)$displayErrorDetails; + } + + /** + * Invoke error handler + * + * @param ServerRequestInterface $request The most recent Request object + * @param ResponseInterface $response The most recent Response object + * @param Exception $exception The caught Exception object + * + * @return ResponseInterface + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, Exception $exception) + { + $contentType = $this->determineContentType($request); + switch ($contentType) { + case 'application/json': + $output = $this->renderJsonErrorMessage($exception); + break; + + case 'text/xml': + case 'application/xml': + $output = $this->renderXmlErrorMessage($exception); + break; + + case 'text/html': + $output = $this->renderHtmlErrorMessage($exception); + break; + } + + $this->writeToErrorLog($exception); + + $body = new Body(fopen('php://temp', 'r+')); + $body->write($output); + + return $response + ->withStatus(500) + ->withHeader('Content-type', $contentType) + ->withBody($body); + } + + + /** + * Write to the error log if displayErrorDetails is false + * + * @param Exception $exception + * @return void + */ + protected function writeToErrorLog($exception) + { + if ($this->displayErrorDetails) { + return; + } + + $message = 'Slim Application Error:' . PHP_EOL; + $message .= $this->renderTextException($exception); + while ($exception = $exception->getPrevious()) { + $message .= PHP_EOL . 'Previous exception:' . PHP_EOL; + $message .= $this->renderTextException($exception); + } + + $message .= PHP_EOL . 'View in rendered output by enabling the "displayErrorDetails" setting.' . PHP_EOL; + + error_log($message); + } + + /** + * Render exception as Text. + * + * @param Exception $exception + * + * @return string + */ + protected function renderTextException(Exception $exception) + { + $text = sprintf('Type: %s' . PHP_EOL, get_class($exception)); + + if (($code = $exception->getCode())) { + $text .= sprintf('Code: %s' . PHP_EOL, $code); + } + + if (($message = $exception->getMessage())) { + $text .= sprintf('Message: %s' . PHP_EOL, htmlentities($message)); + } + + if (($file = $exception->getFile())) { + $text .= sprintf('File: %s' . PHP_EOL, $file); + } + + if (($line = $exception->getLine())) { + $text .= sprintf('Line: %s' . PHP_EOL, $line); + } + + if (($trace = $exception->getTraceAsString())) { + $text .= sprintf('Trace: %s', $trace); + } + + return $text; + } + + /** + * Render HTML error page + * + * @param Exception $exception + * @return string + */ + protected function renderHtmlErrorMessage(Exception $exception) + { + $title = 'Slim Application Error'; + + if ($this->displayErrorDetails) { + $html = '

The application could not run because of the following error:

'; + $html .= '

Details

'; + $html .= $this->renderHtmlException($exception); + + while ($exception = $exception->getPrevious()) { + $html .= '

Previous exception

'; + $html .= $this->renderHtmlException($exception); + } + } else { + $html = '

A website error has occurred. Sorry for the temporary inconvenience.

'; + } + + $output = sprintf( + "" . + "%s

%s

%s", + $title, + $title, + $html + ); + + return $output; + } + + /** + * Render exception as HTML. + * + * @param Exception $exception + * + * @return string + */ + protected function renderHtmlException(Exception $exception) + { + $html = sprintf('
Type: %s
', get_class($exception)); + + if (($code = $exception->getCode())) { + $html .= sprintf('
Code: %s
', $code); + } + + if (($message = $exception->getMessage())) { + $html .= sprintf('
Message: %s
', htmlentities($message)); + } + + if (($file = $exception->getFile())) { + $html .= sprintf('
File: %s
', $file); + } + + if (($line = $exception->getLine())) { + $html .= sprintf('
Line: %s
', $line); + } + + if (($trace = $exception->getTraceAsString())) { + $html .= '

Trace

'; + $html .= sprintf('
%s
', htmlentities($trace)); + } + + return $html; + } + + /** + * Render JSON error + * + * @param Exception $exception + * @return string + */ + protected function renderJsonErrorMessage(Exception $exception) + { + $error = [ + 'message' => 'Slim Application Error', + ]; + + if ($this->displayErrorDetails) { + $error['exception'] = []; + + do { + $error['exception'][] = [ + 'type' => get_class($exception), + 'code' => $exception->getCode(), + 'message' => $exception->getMessage(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => explode("\n", $exception->getTraceAsString()), + ]; + } while ($exception = $exception->getPrevious()); + } + + return json_encode($error, JSON_PRETTY_PRINT); + } + + /** + * Render XML error + * + * @param Exception $exception + * @return string + */ + protected function renderXmlErrorMessage(Exception $exception) + { + $xml = "\n Slim Application Error\n"; + if ($this->displayErrorDetails) { + do { + $xml .= " \n"; + $xml .= " " . get_class($exception) . "\n"; + $xml .= " " . $exception->getCode() . "\n"; + $xml .= " " . $this->createCdataSection($exception->getMessage()) . "\n"; + $xml .= " " . $exception->getFile() . "\n"; + $xml .= " " . $exception->getLine() . "\n"; + $xml .= " " . $this->createCdataSection($exception->getTraceAsString()) . "\n"; + $xml .= " \n"; + } while ($exception = $exception->getPrevious()); + } + $xml .= ""; + + return $xml; + } + + /** + * Returns a CDATA section with the given content. + * + * @param string $content + * @return string + */ + private function createCdataSection($content) + { + return sprintf('', str_replace(']]>', ']]]]>', $content)); + } + + /** + * Determine which content type we know about is wanted using Accept header + * + * @param ServerRequestInterface $request + * @return string + */ + private function determineContentType(ServerRequestInterface $request) + { + $acceptHeader = $request->getHeaderLine('Accept'); + $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); + + if (count($selectedContentTypes)) { + return $selectedContentTypes[0]; + } + + return 'text/html'; + } +} diff --git a/vendor/slim/slim/Slim/Handlers/NotAllowed.php b/vendor/slim/slim/Slim/Handlers/NotAllowed.php new file mode 100644 index 0000000..c018dc6 --- /dev/null +++ b/vendor/slim/slim/Slim/Handlers/NotAllowed.php @@ -0,0 +1,173 @@ +getMethod() === 'OPTIONS') { + $status = 200; + $contentType = 'text/plain'; + $output = $this->renderPlainNotAllowedMessage($methods); + } else { + $status = 405; + $contentType = $this->determineContentType($request); + switch ($contentType) { + case 'application/json': + $output = $this->renderJsonNotAllowedMessage($methods); + break; + + case 'text/xml': + case 'application/xml': + $output = $this->renderXmlNotAllowedMessage($methods); + break; + + case 'text/html': + $output = $this->renderHtmlNotAllowedMessage($methods); + break; + } + } + + $body = new Body(fopen('php://temp', 'r+')); + $body->write($output); + $allow = implode(', ', $methods); + + return $response + ->withStatus($status) + ->withHeader('Content-type', $contentType) + ->withHeader('Allow', $allow) + ->withBody($body); + } + + /** + * Determine which content type we know about is wanted using Accept header + * + * @param ServerRequestInterface $request + * @return string + */ + private function determineContentType(ServerRequestInterface $request) + { + $acceptHeader = $request->getHeaderLine('Accept'); + $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); + + if (count($selectedContentTypes)) { + return $selectedContentTypes[0]; + } + + return 'text/html'; + } + + /** + * Render PLAIN not allowed message + * + * @param array $methods + * @return string + */ + protected function renderPlainNotAllowedMessage($methods) + { + $allow = implode(', ', $methods); + + return 'Allowed methods: ' . $allow; + } + + /** + * Render JSON not allowed message + * + * @param array $methods + * @return string + */ + protected function renderJsonNotAllowedMessage($methods) + { + $allow = implode(', ', $methods); + + return '{"message":"Method not allowed. Must be one of: ' . $allow . '"}'; + } + + /** + * Render XML not allowed message + * + * @param array $methods + * @return string + */ + protected function renderXmlNotAllowedMessage($methods) + { + $allow = implode(', ', $methods); + + return "Method not allowed. Must be one of: $allow"; + } + + /** + * Render HTML not allowed message + * + * @param array $methods + * @return string + */ + protected function renderHtmlNotAllowedMessage($methods) + { + $allow = implode(', ', $methods); + $output = << + + Method not allowed + + + +

Method not allowed

+

Method not allowed. Must be one of: $allow

+ + +END; + + return $output; + } +} diff --git a/vendor/slim/slim/Slim/Handlers/NotFound.php b/vendor/slim/slim/Slim/Handlers/NotFound.php new file mode 100644 index 0000000..d668199 --- /dev/null +++ b/vendor/slim/slim/Slim/Handlers/NotFound.php @@ -0,0 +1,157 @@ +determineContentType($request); + switch ($contentType) { + case 'application/json': + $output = $this->renderJsonNotFoundOutput($request, $response); + break; + + case 'text/xml': + case 'application/xml': + $output = $this->renderXmlNotFoundOutput($request, $response); + break; + + case 'text/html': + $output = $this->renderHtmlNotFoundOutput($request, $response); + } + + $body = new Body(fopen('php://temp', 'r+')); + $body->write($output); + + return $response->withStatus(404) + ->withHeader('Content-Type', $contentType) + ->withBody($body); + } + + /** + * Determine which content type we know about is wanted using Accept header + * + * @param ServerRequestInterface $request + * @return string + */ + private function determineContentType(ServerRequestInterface $request) + { + $acceptHeader = $request->getHeaderLine('Accept'); + $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); + + if (count($selectedContentTypes)) { + return $selectedContentTypes[0]; + } + + return 'text/html'; + } + + /** + * Return a response for application/json content not found + * + * @param ServerRequestInterface $request The most recent Request object + * @param ResponseInterface $response The most recent Response object + * + * @return ResponseInterface + */ + protected function renderJsonNotFoundOutput(ServerRequestInterface $request, ResponseInterface $response) + { + return '{"message":"Not found"}'; + } + + /** + * Return a response for xml content not found + * + * @param ServerRequestInterface $request The most recent Request object + * @param ResponseInterface $response The most recent Response object + * + * @return ResponseInterface + */ + protected function renderXmlNotFoundOutput(ServerRequestInterface $request, ResponseInterface $response) + { + return 'Not found'; + } + + /** + * Return a response for text/html content not found + * + * @param ServerRequestInterface $request The most recent Request object + * @param ResponseInterface $response The most recent Response object + * + * @return ResponseInterface + */ + protected function renderHtmlNotFoundOutput(ServerRequestInterface $request, ResponseInterface $response) + { + $homeUrl = (string)($request->getUri()->withPath('')->withQuery('')->withFragment('')); + return << + + Page Not Found + + + +

Page Not Found

+

+ The page you are looking for could not be found. Check the address bar + to ensure your URL is spelled correctly. If all else fails, you can + visit our home page at the link below. +

+ Visit the Home Page + + +END; + } +} diff --git a/vendor/slim/slim/Slim/Handlers/PhpError.php b/vendor/slim/slim/Slim/Handlers/PhpError.php new file mode 100644 index 0000000..4f3faf8 --- /dev/null +++ b/vendor/slim/slim/Slim/Handlers/PhpError.php @@ -0,0 +1,303 @@ +displayErrorDetails = (bool)$displayErrorDetails; + } + + /** + * Invoke error handler + * + * @param ServerRequestInterface $request The most recent Request object + * @param ResponseInterface $response The most recent Response object + * @param Throwable $error The caught Throwable object + * + * @return ResponseInterface + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, Throwable $error) + { + $contentType = $this->determineContentType($request); + switch ($contentType) { + case 'application/json': + $output = $this->renderJsonErrorMessage($error); + break; + + case 'text/xml': + case 'application/xml': + $output = $this->renderXmlErrorMessage($error); + break; + + case 'text/html': + $output = $this->renderHtmlErrorMessage($error); + break; + } + + $this->writeToErrorLog($error); + + $body = new Body(fopen('php://temp', 'r+')); + $body->write($output); + + return $response + ->withStatus(500) + ->withHeader('Content-type', $contentType) + ->withBody($body); + } + + + /** + * Write to the error log if displayErrorDetails is false + * + * @param Throwable $error + * + * @return void + */ + protected function writeToErrorLog($error) + { + if ($this->displayErrorDetails) { + return; + } + + $message = 'Slim Application Error:' . PHP_EOL; + $message .= $this->renderTextError($error); + while ($error = $error->getPrevious()) { + $message .= PHP_EOL . 'Previous error:' . PHP_EOL; + $message .= $this->renderTextError($error); + } + + $message .= PHP_EOL . 'View in rendered output by enabling the "displayErrorDetails" setting.' . PHP_EOL; + + error_log($message); + } + + /** + * Render error as Text. + * + * @param Throwable $error + * + * @return string + */ + protected function renderTextError(Throwable $error) + { + $text = sprintf('Type: %s' . PHP_EOL, get_class($error)); + + if (($code = $error->getCode())) { + $text .= sprintf('Code: %s' . PHP_EOL, $code); + } + + if (($message = $error->getMessage())) { + $text .= sprintf('Message: %s' . PHP_EOL, htmlentities($message)); + } + + if (($file = $error->getFile())) { + $text .= sprintf('File: %s' . PHP_EOL, $file); + } + + if (($line = $error->getLine())) { + $text .= sprintf('Line: %s' . PHP_EOL, $line); + } + + if (($trace = $error->getTraceAsString())) { + $text .= sprintf('Trace: %s', $trace); + } + + return $text; + } + + /** + * Render HTML error page + * + * @param Throwable $error + * + * @return string + */ + protected function renderHtmlErrorMessage(Throwable $error) + { + $title = 'Slim Application Error'; + + if ($this->displayErrorDetails) { + $html = '

The application could not run because of the following error:

'; + $html .= '

Details

'; + $html .= $this->renderHtmlError($error); + + while ($error = $error->getPrevious()) { + $html .= '

Previous error

'; + $html .= $this->renderHtmlError($error); + } + } else { + $html = '

A website error has occurred. Sorry for the temporary inconvenience.

'; + } + + $output = sprintf( + "" . + "%s

%s

%s", + $title, + $title, + $html + ); + + return $output; + } + + /** + * Render error as HTML. + * + * @param Throwable $error + * + * @return string + */ + protected function renderHtmlError(Throwable $error) + { + $html = sprintf('
Type: %s
', get_class($error)); + + if (($code = $error->getCode())) { + $html .= sprintf('
Code: %s
', $code); + } + + if (($message = $error->getMessage())) { + $html .= sprintf('
Message: %s
', htmlentities($message)); + } + + if (($file = $error->getFile())) { + $html .= sprintf('
File: %s
', $file); + } + + if (($line = $error->getLine())) { + $html .= sprintf('
Line: %s
', $line); + } + + if (($trace = $error->getTraceAsString())) { + $html .= '

Trace

'; + $html .= sprintf('
%s
', htmlentities($trace)); + } + + return $html; + } + + /** + * Render JSON error + * + * @param Throwable $error + * + * @return string + */ + protected function renderJsonErrorMessage(Throwable $error) + { + $json = [ + 'message' => 'Slim Application Error', + ]; + + if ($this->displayErrorDetails) { + $json['error'] = []; + + do { + $json['error'][] = [ + 'type' => get_class($error), + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + 'file' => $error->getFile(), + 'line' => $error->getLine(), + 'trace' => explode("\n", $error->getTraceAsString()), + ]; + } while ($error = $error->getPrevious()); + } + + return json_encode($json, JSON_PRETTY_PRINT); + } + + /** + * Render XML error + * + * @param Throwable $error + * @return string + */ + protected function renderXmlErrorMessage(Throwable $error) + { + $xml = "\n Slim Application Error\n"; + if ($this->displayErrorDetails) { + do { + $xml .= " \n"; + $xml .= " " . get_class($error) . "\n"; + $xml .= " " . $error->getCode() . "\n"; + $xml .= " " . $this->createCdataSection($error->getMessage()) . "\n"; + $xml .= " " . $error->getFile() . "\n"; + $xml .= " " . $error->getLine() . "\n"; + $xml .= " " . $this->createCdataSection($error->getTraceAsString()) . "\n"; + $xml .= " \n"; + } while ($error = $error->getPrevious()); + } + $xml .= ""; + + return $xml; + } + + /** + * Returns a CDATA section with the given content. + * + * @param string $content + * @return string + */ + private function createCdataSection($content) + { + return sprintf('', str_replace(']]>', ']]]]>', $content)); + } + + /** + * Determine which content type we know about is wanted using Accept header + * + * @param ServerRequestInterface $request + * @return string + */ + private function determineContentType(ServerRequestInterface $request) + { + $acceptHeader = $request->getHeaderLine('Accept'); + $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); + + if (count($selectedContentTypes)) { + return $selectedContentTypes[0]; + } + + return 'text/html'; + } +} diff --git a/vendor/slim/slim/Slim/Handlers/Strategies/RequestResponse.php b/vendor/slim/slim/Slim/Handlers/Strategies/RequestResponse.php new file mode 100644 index 0000000..157bdeb --- /dev/null +++ b/vendor/slim/slim/Slim/Handlers/Strategies/RequestResponse.php @@ -0,0 +1,43 @@ + $v) { + $request = $request->withAttribute($k, $v); + } + + return call_user_func($callable, $request, $response, $routeArguments); + } +} diff --git a/vendor/slim/slim/Slim/Handlers/Strategies/RequestResponseArgs.php b/vendor/slim/slim/Slim/Handlers/Strategies/RequestResponseArgs.php new file mode 100644 index 0000000..11793d3 --- /dev/null +++ b/vendor/slim/slim/Slim/Handlers/Strategies/RequestResponseArgs.php @@ -0,0 +1,42 @@ + '', + 'domain' => null, + 'hostonly' => null, + 'path' => null, + 'expires' => null, + 'secure' => false, + 'httponly' => false + ]; + + /** + * Create new cookies helper + * + * @param array $cookies + */ + public function __construct(array $cookies = []) + { + $this->requestCookies = $cookies; + } + + /** + * Set default cookie properties + * + * @param array $settings + */ + public function setDefaults(array $settings) + { + $this->defaults = array_replace($this->defaults, $settings); + } + + /** + * Get request cookie + * + * @param string $name Cookie name + * @param mixed $default Cookie default value + * + * @return mixed Cookie value if present, else default + */ + public function get($name, $default = null) + { + return isset($this->requestCookies[$name]) ? $this->requestCookies[$name] : $default; + } + + /** + * Set response cookie + * + * @param string $name Cookie name + * @param string|array $value Cookie value, or cookie properties + */ + public function set($name, $value) + { + if (!is_array($value)) { + $value = ['value' => (string)$value]; + } + $this->responseCookies[$name] = array_replace($this->defaults, $value); + } + + /** + * Convert to `Set-Cookie` headers + * + * @return string[] + */ + public function toHeaders() + { + $headers = []; + foreach ($this->responseCookies as $name => $properties) { + $headers[] = $this->toHeader($name, $properties); + } + + return $headers; + } + + /** + * Convert to `Set-Cookie` header + * + * @param string $name Cookie name + * @param array $properties Cookie properties + * + * @return string + */ + protected function toHeader($name, array $properties) + { + $result = urlencode($name) . '=' . urlencode($properties['value']); + + if (isset($properties['domain'])) { + $result .= '; domain=' . $properties['domain']; + } + + if (isset($properties['path'])) { + $result .= '; path=' . $properties['path']; + } + + if (isset($properties['expires'])) { + if (is_string($properties['expires'])) { + $timestamp = strtotime($properties['expires']); + } else { + $timestamp = (int)$properties['expires']; + } + if ($timestamp !== 0) { + $result .= '; expires=' . gmdate('D, d-M-Y H:i:s e', $timestamp); + } + } + + if (isset($properties['secure']) && $properties['secure']) { + $result .= '; secure'; + } + + if (isset($properties['hostonly']) && $properties['hostonly']) { + $result .= '; HostOnly'; + } + + if (isset($properties['httponly']) && $properties['httponly']) { + $result .= '; HttpOnly'; + } + + return $result; + } + + /** + * Parse HTTP request `Cookie:` header and extract + * into a PHP associative array. + * + * @param string $header The raw HTTP request `Cookie:` header + * + * @return array Associative array of cookie names and values + * + * @throws InvalidArgumentException if the cookie data cannot be parsed + */ + public static function parseHeader($header) + { + if (is_array($header) === true) { + $header = isset($header[0]) ? $header[0] : ''; + } + + if (is_string($header) === false) { + throw new InvalidArgumentException('Cannot parse Cookie data. Header value must be a string.'); + } + + $header = rtrim($header, "\r\n"); + $pieces = preg_split('@\s*[;,]\s*@', $header); + $cookies = []; + + foreach ($pieces as $cookie) { + $cookie = explode('=', $cookie, 2); + + if (count($cookie) === 2) { + $key = urldecode($cookie[0]); + $value = urldecode($cookie[1]); + + if (!isset($cookies[$key])) { + $cookies[$key] = $value; + } + } + } + + return $cookies; + } +} diff --git a/vendor/slim/slim/Slim/Http/Environment.php b/vendor/slim/slim/Slim/Http/Environment.php new file mode 100644 index 0000000..a106fa8 --- /dev/null +++ b/vendor/slim/slim/Slim/Http/Environment.php @@ -0,0 +1,52 @@ + 'HTTP/1.1', + 'REQUEST_METHOD' => 'GET', + 'SCRIPT_NAME' => '', + 'REQUEST_URI' => '', + 'QUERY_STRING' => '', + 'SERVER_NAME' => 'localhost', + 'SERVER_PORT' => 80, + 'HTTP_HOST' => 'localhost', + 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.8', + 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.3', + 'HTTP_USER_AGENT' => 'Slim Framework', + 'REMOTE_ADDR' => '127.0.0.1', + 'REQUEST_TIME' => time(), + 'REQUEST_TIME_FLOAT' => microtime(true), + ], $userData); + + return new static($data); + } +} diff --git a/vendor/slim/slim/Slim/Http/Headers.php b/vendor/slim/slim/Slim/Http/Headers.php new file mode 100644 index 0000000..6b6375b --- /dev/null +++ b/vendor/slim/slim/Slim/Http/Headers.php @@ -0,0 +1,197 @@ + 1, + 'CONTENT_LENGTH' => 1, + 'PHP_AUTH_USER' => 1, + 'PHP_AUTH_PW' => 1, + 'PHP_AUTH_DIGEST' => 1, + 'AUTH_TYPE' => 1, + ]; + + /** + * Create new headers collection with data extracted from + * the application Environment object + * + * @param Environment $environment The Slim application Environment + * + * @return self + */ + public static function createFromEnvironment(Environment $environment) + { + $data = []; + foreach ($environment as $key => $value) { + $key = strtoupper($key); + if (isset(static::$special[$key]) || strpos($key, 'HTTP_') === 0) { + if ($key !== 'HTTP_CONTENT_LENGTH') { + $data[$key] = $value; + } + } + } + + return new static($data); + } + + /** + * Return array of HTTP header names and values. + * This method returns the _original_ header name + * as specified by the end user. + * + * @return array + */ + public function all() + { + $all = parent::all(); + $out = []; + foreach ($all as $key => $props) { + $out[$props['originalKey']] = $props['value']; + } + + return $out; + } + + /** + * Set HTTP header value + * + * This method sets a header value. It replaces + * any values that may already exist for the header name. + * + * @param string $key The case-insensitive header name + * @param string $value The header value + */ + public function set($key, $value) + { + if (!is_array($value)) { + $value = [$value]; + } + parent::set($this->normalizeKey($key), [ + 'value' => $value, + 'originalKey' => $key + ]); + } + + /** + * Get HTTP header value + * + * @param string $key The case-insensitive header name + * @param mixed $default The default value if key does not exist + * + * @return string[] + */ + public function get($key, $default = null) + { + if ($this->has($key)) { + return parent::get($this->normalizeKey($key))['value']; + } + + return $default; + } + + /** + * Get HTTP header key as originally specified + * + * @param string $key The case-insensitive header name + * @param mixed $default The default value if key does not exist + * + * @return string + */ + public function getOriginalKey($key, $default = null) + { + if ($this->has($key)) { + return parent::get($this->normalizeKey($key))['originalKey']; + } + + return $default; + } + + /** + * Add HTTP header value + * + * This method appends a header value. Unlike the set() method, + * this method _appends_ this new value to any values + * that already exist for this header name. + * + * @param string $key The case-insensitive header name + * @param array|string $value The new header value(s) + */ + public function add($key, $value) + { + $oldValues = $this->get($key, []); + $newValues = is_array($value) ? $value : [$value]; + $this->set($key, array_merge($oldValues, array_values($newValues))); + } + + /** + * Does this collection have a given header? + * + * @param string $key The case-insensitive header name + * + * @return bool + */ + public function has($key) + { + return parent::has($this->normalizeKey($key)); + } + + /** + * Remove header from collection + * + * @param string $key The case-insensitive header name + */ + public function remove($key) + { + parent::remove($this->normalizeKey($key)); + } + + /** + * Normalize header name + * + * This method transforms header names into a + * normalized form. This is how we enable case-insensitive + * header names in the other methods in this class. + * + * @param string $key The case-insensitive header name + * + * @return string Normalized header name + */ + public function normalizeKey($key) + { + $key = strtr(strtolower($key), '_', '-'); + if (strpos($key, 'http-') === 0) { + $key = substr($key, 5); + } + + return $key; + } +} diff --git a/vendor/slim/slim/Slim/Http/Message.php b/vendor/slim/slim/Slim/Http/Message.php new file mode 100644 index 0000000..d0e832d --- /dev/null +++ b/vendor/slim/slim/Slim/Http/Message.php @@ -0,0 +1,295 @@ +protocolVersion; + } + + /** + * Return an instance with the specified HTTP protocol version. + * + * The version string MUST contain only the HTTP version number (e.g., + * "1.1", "1.0"). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new protocol version. + * + * @param string $version HTTP protocol version + * @return static + * @throws InvalidArgumentException if the http version is an invalid number + */ + public function withProtocolVersion($version) + { + static $valid = [ + '1.0' => true, + '1.1' => true, + '2.0' => true, + ]; + if (!isset($valid[$version])) { + throw new InvalidArgumentException('Invalid HTTP version. Must be one of: 1.0, 1.1, 2.0'); + } + $clone = clone $this; + $clone->protocolVersion = $version; + + return $clone; + } + + /******************************************************************************* + * Headers + ******************************************************************************/ + + /** + * Retrieves all message header values. + * + * The keys represent the header name as it will be sent over the wire, and + * each value is an array of strings associated with the header. + * + * // Represent the headers as a string + * foreach ($message->getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return array Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings + * for that header. + */ + public function getHeaders() + { + return $this->headers->all(); + } + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader($name) + { + return $this->headers->has($name); + } + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader($name) + { + return $this->headers->get($name, []); + } + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function getHeaderLine($name) + { + return implode(',', $this->headers->get($name, [])); + } + + /** + * Return an instance with the provided value replacing the specified header. + * + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withHeader($name, $value) + { + $clone = clone $this; + $clone->headers->set($name, $value); + + return $clone; + } + + /** + * Return an instance with the specified header appended with the given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new header and/or value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withAddedHeader($name, $value) + { + $clone = clone $this; + $clone->headers->add($name, $value); + + return $clone; + } + + /** + * Return an instance without the specified header. + * + * Header resolution MUST be done without case-sensitivity. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the named header. + * + * @param string $name Case-insensitive header field name to remove. + * @return static + */ + public function withoutHeader($name) + { + $clone = clone $this; + $clone->headers->remove($name); + + return $clone; + } + + /******************************************************************************* + * Body + ******************************************************************************/ + + /** + * Gets the body of the message. + * + * @return StreamInterface Returns the body as a stream. + */ + public function getBody() + { + return $this->body; + } + + /** + * Return an instance with the specified message body. + * + * The body MUST be a StreamInterface object. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new body stream. + * + * @param StreamInterface $body Body. + * @return static + * @throws \InvalidArgumentException When the body is not valid. + */ + public function withBody(StreamInterface $body) + { + // TODO: Test for invalid body? + $clone = clone $this; + $clone->body = $body; + + return $clone; + } +} diff --git a/vendor/slim/slim/Slim/Http/Request.php b/vendor/slim/slim/Slim/Http/Request.php new file mode 100644 index 0000000..67585d6 --- /dev/null +++ b/vendor/slim/slim/Slim/Http/Request.php @@ -0,0 +1,1150 @@ + 1, + 'DELETE' => 1, + 'GET' => 1, + 'HEAD' => 1, + 'OPTIONS' => 1, + 'PATCH' => 1, + 'POST' => 1, + 'PUT' => 1, + 'TRACE' => 1, + ]; + + /** + * Create new HTTP request with data extracted from the application + * Environment object + * + * @param Environment $environment The Slim application Environment + * + * @return self + */ + public static function createFromEnvironment(Environment $environment) + { + $method = $environment['REQUEST_METHOD']; + $uri = Uri::createFromEnvironment($environment); + $headers = Headers::createFromEnvironment($environment); + $cookies = Cookies::parseHeader($headers->get('Cookie', [])); + $serverParams = $environment->all(); + $body = new RequestBody(); + $uploadedFiles = UploadedFile::createFromEnvironment($environment); + + $request = new static($method, $uri, $headers, $cookies, $serverParams, $body, $uploadedFiles); + + if ($method === 'POST' && + in_array($request->getMediaType(), ['application/x-www-form-urlencoded', 'multipart/form-data']) + ) { + // parsed body must be $_POST + $request = $request->withParsedBody($_POST); + } + return $request; + } + + /** + * Create new HTTP request. + * + * Adds a host header when none was provided and a host is defined in uri. + * + * @param string $method The request method + * @param UriInterface $uri The request URI object + * @param HeadersInterface $headers The request headers collection + * @param array $cookies The request cookies collection + * @param array $serverParams The server environment variables + * @param StreamInterface $body The request body object + * @param array $uploadedFiles The request uploadedFiles collection + */ + public function __construct( + $method, + UriInterface $uri, + HeadersInterface $headers, + array $cookies, + array $serverParams, + StreamInterface $body, + array $uploadedFiles = [] + ) { + $this->originalMethod = $this->filterMethod($method); + $this->uri = $uri; + $this->headers = $headers; + $this->cookies = $cookies; + $this->serverParams = $serverParams; + $this->attributes = new Collection(); + $this->body = $body; + $this->uploadedFiles = $uploadedFiles; + + if (isset($serverParams['SERVER_PROTOCOL'])) { + $this->protocolVersion = str_replace('HTTP/', '', $serverParams['SERVER_PROTOCOL']); + } + + if (!$this->headers->has('Host') || $this->uri->getHost() !== '') { + $this->headers->set('Host', $this->uri->getHost()); + } + + $this->registerMediaTypeParser('application/json', function ($input) { + return json_decode($input, true); + }); + + $this->registerMediaTypeParser('application/xml', function ($input) { + $backup = libxml_disable_entity_loader(true); + $result = simplexml_load_string($input); + libxml_disable_entity_loader($backup); + return $result; + }); + + $this->registerMediaTypeParser('text/xml', function ($input) { + $backup = libxml_disable_entity_loader(true); + $result = simplexml_load_string($input); + libxml_disable_entity_loader($backup); + return $result; + }); + + $this->registerMediaTypeParser('application/x-www-form-urlencoded', function ($input) { + parse_str($input, $data); + return $data; + }); + } + + /** + * This method is applied to the cloned object + * after PHP performs an initial shallow-copy. This + * method completes a deep-copy by creating new objects + * for the cloned object's internal reference pointers. + */ + public function __clone() + { + $this->headers = clone $this->headers; + $this->attributes = clone $this->attributes; + $this->body = clone $this->body; + } + + /******************************************************************************* + * Method + ******************************************************************************/ + + /** + * Retrieves the HTTP method of the request. + * + * @return string Returns the request method. + */ + public function getMethod() + { + if ($this->method === null) { + $this->method = $this->originalMethod; + $customMethod = $this->getHeaderLine('X-Http-Method-Override'); + + if ($customMethod) { + $this->method = $this->filterMethod($customMethod); + } elseif ($this->originalMethod === 'POST') { + $body = $this->getParsedBody(); + + if (is_object($body) && property_exists($body, '_METHOD')) { + $this->method = $this->filterMethod((string)$body->_METHOD); + } elseif (is_array($body) && isset($body['_METHOD'])) { + $this->method = $this->filterMethod((string)$body['_METHOD']); + } + + if ($this->getBody()->eof()) { + $this->getBody()->rewind(); + } + } + } + + return $this->method; + } + + /** + * Get the original HTTP method (ignore override). + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string + */ + public function getOriginalMethod() + { + return $this->originalMethod; + } + + /** + * Return an instance with the provided HTTP method. + * + * While HTTP method names are typically all uppercase characters, HTTP + * method names are case-sensitive and thus implementations SHOULD NOT + * modify the given string. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * changed request method. + * + * @param string $method Case-sensitive method. + * @return self + * @throws \InvalidArgumentException for invalid HTTP methods. + */ + public function withMethod($method) + { + $method = $this->filterMethod($method); + $clone = clone $this; + $clone->originalMethod = $method; + $clone->method = $method; + + return $clone; + } + + /** + * Validate the HTTP method + * + * @param null|string $method + * @return null|string + * @throws \InvalidArgumentException on invalid HTTP method. + */ + protected function filterMethod($method) + { + if ($method === null) { + return $method; + } + + if (!is_string($method)) { + throw new InvalidArgumentException(sprintf( + 'Unsupported HTTP method; must be a string, received %s', + (is_object($method) ? get_class($method) : gettype($method)) + )); + } + + $method = strtoupper($method); + if (!isset($this->validMethods[$method])) { + throw new InvalidArgumentException(sprintf( + 'Unsupported HTTP method "%s" provided', + $method + )); + } + + return $method; + } + + /** + * Does this request use a given method? + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $method HTTP method + * @return bool + */ + public function isMethod($method) + { + return $this->getMethod() === $method; + } + + /** + * Is this a GET request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isGet() + { + return $this->isMethod('GET'); + } + + /** + * Is this a POST request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isPost() + { + return $this->isMethod('POST'); + } + + /** + * Is this a PUT request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isPut() + { + return $this->isMethod('PUT'); + } + + /** + * Is this a PATCH request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isPatch() + { + return $this->isMethod('PATCH'); + } + + /** + * Is this a DELETE request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isDelete() + { + return $this->isMethod('DELETE'); + } + + /** + * Is this a HEAD request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isHead() + { + return $this->isMethod('HEAD'); + } + + /** + * Is this a OPTIONS request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isOptions() + { + return $this->isMethod('OPTIONS'); + } + + /** + * Is this an XHR request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isXhr() + { + return $this->getHeaderLine('X-Requested-With') === 'XMLHttpRequest'; + } + + /******************************************************************************* + * URI + ******************************************************************************/ + + /** + * Retrieves the message's request target. + * + * Retrieves the message's request-target either as it will appear (for + * clients), as it appeared at request (for servers), or as it was + * specified for the instance (see withRequestTarget()). + * + * In most cases, this will be the origin-form of the composed URI, + * unless a value was provided to the concrete implementation (see + * withRequestTarget() below). + * + * If no URI is available, and no request-target has been specifically + * provided, this method MUST return the string "/". + * + * @return string + */ + public function getRequestTarget() + { + if ($this->requestTarget) { + return $this->requestTarget; + } + + if ($this->uri === null) { + return '/'; + } + + $basePath = $this->uri->getBasePath(); + $path = $this->uri->getPath(); + $path = $basePath . '/' . ltrim($path, '/'); + + $query = $this->uri->getQuery(); + if ($query) { + $path .= '?' . $query; + } + $this->requestTarget = $path; + + return $this->requestTarget; + } + + /** + * Return an instance with the specific request-target. + * + * If the request needs a non-origin-form request-target — e.g., for + * specifying an absolute-form, authority-form, or asterisk-form — + * this method may be used to create an instance with the specified + * request-target, verbatim. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * changed request target. + * + * @link http://tools.ietf.org/html/rfc7230#section-2.7 (for the various + * request-target forms allowed in request messages) + * @param mixed $requestTarget + * @return self + * @throws InvalidArgumentException if the request target is invalid + */ + public function withRequestTarget($requestTarget) + { + if (preg_match('#\s#', $requestTarget)) { + throw new InvalidArgumentException( + 'Invalid request target provided; must be a string and cannot contain whitespace' + ); + } + $clone = clone $this; + $clone->requestTarget = $requestTarget; + + return $clone; + } + + /** + * Retrieves the URI instance. + * + * This method MUST return a UriInterface instance. + * + * @link http://tools.ietf.org/html/rfc3986#section-4.3 + * @return UriInterface Returns a UriInterface instance + * representing the URI of the request. + */ + public function getUri() + { + return $this->uri; + } + + /** + * Returns an instance with the provided URI. + * + * This method MUST update the Host header of the returned request by + * default if the URI contains a host component. If the URI does not + * contain a host component, any pre-existing Host header MUST be carried + * over to the returned request. + * + * You can opt-in to preserving the original state of the Host header by + * setting `$preserveHost` to `true`. When `$preserveHost` is set to + * `true`, this method interacts with the Host header in the following ways: + * + * - If the the Host header is missing or empty, and the new URI contains + * a host component, this method MUST update the Host header in the returned + * request. + * - If the Host header is missing or empty, and the new URI does not contain a + * host component, this method MUST NOT update the Host header in the returned + * request. + * - If a Host header is present and non-empty, this method MUST NOT update + * the Host header in the returned request. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new UriInterface instance. + * + * @link http://tools.ietf.org/html/rfc3986#section-4.3 + * @param UriInterface $uri New request URI to use. + * @param bool $preserveHost Preserve the original state of the Host header. + * @return self + */ + public function withUri(UriInterface $uri, $preserveHost = false) + { + $clone = clone $this; + $clone->uri = $uri; + + if (!$preserveHost) { + if ($uri->getHost() !== '') { + $clone->headers->set('Host', $uri->getHost()); + } + } else { + if ($this->uri->getHost() !== '' && (!$this->hasHeader('Host') || $this->getHeader('Host') === null)) { + $clone->headers->set('Host', $uri->getHost()); + } + } + + return $clone; + } + + /** + * Get request content type. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string|null The request content type, if known + */ + public function getContentType() + { + $result = $this->getHeader('Content-Type'); + + return $result ? $result[0] : null; + } + + /** + * Get request media type, if known. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string|null The request media type, minus content-type params + */ + public function getMediaType() + { + $contentType = $this->getContentType(); + if ($contentType) { + $contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType); + + return strtolower($contentTypeParts[0]); + } + + return null; + } + + /** + * Get request media type params, if known. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return array + */ + public function getMediaTypeParams() + { + $contentType = $this->getContentType(); + $contentTypeParams = []; + if ($contentType) { + $contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType); + $contentTypePartsLength = count($contentTypeParts); + for ($i = 1; $i < $contentTypePartsLength; $i++) { + $paramParts = explode('=', $contentTypeParts[$i]); + $contentTypeParams[strtolower($paramParts[0])] = $paramParts[1]; + } + } + + return $contentTypeParams; + } + + /** + * Get request content character set, if known. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string|null + */ + public function getContentCharset() + { + $mediaTypeParams = $this->getMediaTypeParams(); + if (isset($mediaTypeParams['charset'])) { + return $mediaTypeParams['charset']; + } + + return null; + } + + /** + * Get request content length, if known. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return int|null + */ + public function getContentLength() + { + $result = $this->headers->get('Content-Length'); + + return $result ? (int)$result[0] : null; + } + + /******************************************************************************* + * Cookies + ******************************************************************************/ + + /** + * Retrieve cookies. + * + * Retrieves cookies sent by the client to the server. + * + * The data MUST be compatible with the structure of the $_COOKIE + * superglobal. + * + * @return array + */ + public function getCookieParams() + { + return $this->cookies; + } + + /** + * Return an instance with the specified cookies. + * + * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST + * be compatible with the structure of $_COOKIE. Typically, this data will + * be injected at instantiation. + * + * This method MUST NOT update the related Cookie header of the request + * instance, nor related values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated cookie values. + * + * @param array $cookies Array of key/value pairs representing cookies. + * @return self + */ + public function withCookieParams(array $cookies) + { + $clone = clone $this; + $clone->cookies = $cookies; + + return $clone; + } + + /******************************************************************************* + * Query Params + ******************************************************************************/ + + /** + * Retrieve query string arguments. + * + * Retrieves the deserialized query string arguments, if any. + * + * Note: the query params might not be in sync with the URI or server + * params. If you need to ensure you are only getting the original + * values, you may need to parse the query string from `getUri()->getQuery()` + * or from the `QUERY_STRING` server param. + * + * @return array + */ + public function getQueryParams() + { + if (is_array($this->queryParams)) { + return $this->queryParams; + } + + if ($this->uri === null) { + return []; + } + + parse_str($this->uri->getQuery(), $this->queryParams); // <-- URL decodes data + + return $this->queryParams; + } + + /** + * Return an instance with the specified query string arguments. + * + * These values SHOULD remain immutable over the course of the incoming + * request. They MAY be injected during instantiation, such as from PHP's + * $_GET superglobal, or MAY be derived from some other value such as the + * URI. In cases where the arguments are parsed from the URI, the data + * MUST be compatible with what PHP's parse_str() would return for + * purposes of how duplicate query parameters are handled, and how nested + * sets are handled. + * + * Setting query string arguments MUST NOT change the URI stored by the + * request, nor the values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated query string arguments. + * + * @param array $query Array of query string arguments, typically from + * $_GET. + * @return self + */ + public function withQueryParams(array $query) + { + $clone = clone $this; + $clone->queryParams = $query; + + return $clone; + } + + /******************************************************************************* + * File Params + ******************************************************************************/ + + /** + * Retrieve normalized file upload data. + * + * This method returns upload metadata in a normalized tree, with each leaf + * an instance of Psr\Http\Message\UploadedFileInterface. + * + * These values MAY be prepared from $_FILES or the message body during + * instantiation, or MAY be injected via withUploadedFiles(). + * + * @return array An array tree of UploadedFileInterface instances; an empty + * array MUST be returned if no data is present. + */ + public function getUploadedFiles() + { + return $this->uploadedFiles; + } + + /** + * Create a new instance with the specified uploaded files. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param array $uploadedFiles An array tree of UploadedFileInterface instances. + * @return self + * @throws \InvalidArgumentException if an invalid structure is provided. + */ + public function withUploadedFiles(array $uploadedFiles) + { + $clone = clone $this; + $clone->uploadedFiles = $uploadedFiles; + + return $clone; + } + + /******************************************************************************* + * Server Params + ******************************************************************************/ + + /** + * Retrieve server parameters. + * + * Retrieves data related to the incoming request environment, + * typically derived from PHP's $_SERVER superglobal. The data IS NOT + * REQUIRED to originate from $_SERVER. + * + * @return array + */ + public function getServerParams() + { + return $this->serverParams; + } + + /******************************************************************************* + * Attributes + ******************************************************************************/ + + /** + * Retrieve attributes derived from the request. + * + * The request "attributes" may be used to allow injection of any + * parameters derived from the request: e.g., the results of path + * match operations; the results of decrypting cookies; the results of + * deserializing non-form-encoded message bodies; etc. Attributes + * will be application and request specific, and CAN be mutable. + * + * @return array Attributes derived from the request. + */ + public function getAttributes() + { + return $this->attributes->all(); + } + + /** + * Retrieve a single derived request attribute. + * + * Retrieves a single derived request attribute as described in + * getAttributes(). If the attribute has not been previously set, returns + * the default value as provided. + * + * This method obviates the need for a hasAttribute() method, as it allows + * specifying a default value to return if the attribute is not found. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * @return mixed + */ + public function getAttribute($name, $default = null) + { + return $this->attributes->get($name, $default); + } + + /** + * Return an instance with the specified derived request attribute. + * + * This method allows setting a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $value The value of the attribute. + * @return self + */ + public function withAttribute($name, $value) + { + $clone = clone $this; + $clone->attributes->set($name, $value); + + return $clone; + } + + /** + * Create a new instance with the specified derived request attributes. + * + * Note: This method is not part of the PSR-7 standard. + * + * This method allows setting all new derived request attributes as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * updated attributes. + * + * @param array $attributes New attributes + * @return self + */ + public function withAttributes(array $attributes) + { + $clone = clone $this; + $clone->attributes = new Collection($attributes); + + return $clone; + } + + /** + * Return an instance that removes the specified derived request attribute. + * + * This method allows removing a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @return self + */ + public function withoutAttribute($name) + { + $clone = clone $this; + $clone->attributes->remove($name); + + return $clone; + } + + /******************************************************************************* + * Body + ******************************************************************************/ + + /** + * Retrieve any parameters provided in the request body. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, this method MUST + * return the contents of $_POST. + * + * Otherwise, this method may return any results of deserializing + * the request body content; as parsing returns structured content, the + * potential types MUST be arrays or objects only. A null value indicates + * the absence of body content. + * + * @return null|array|object The deserialized body parameters, if any. + * These will typically be an array or object. + * @throws RuntimeException if the request body media type parser returns an invalid value + */ + public function getParsedBody() + { + if ($this->bodyParsed !== false) { + return $this->bodyParsed; + } + + if (!$this->body) { + return null; + } + + $mediaType = $this->getMediaType(); + $body = (string)$this->getBody(); + + if (isset($this->bodyParsers[$mediaType]) === true) { + $parsed = $this->bodyParsers[$mediaType]($body); + + if (!is_null($parsed) && !is_object($parsed) && !is_array($parsed)) { + throw new RuntimeException( + 'Request body media type parser return value must be an array, an object, or null' + ); + } + $this->bodyParsed = $parsed; + return $this->bodyParsed; + } + + return null; + } + + /** + * Return an instance with the specified body parameters. + * + * These MAY be injected during instantiation. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, use this method + * ONLY to inject the contents of $_POST. + * + * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of + * deserializing the request body content. Deserialization/parsing returns + * structured data, and, as such, this method ONLY accepts arrays or objects, + * or a null value if nothing was available to parse. + * + * As an example, if content negotiation determines that the request data + * is a JSON payload, this method could be used to create a request + * instance with the deserialized parameters. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param null|array|object $data The deserialized body data. This will + * typically be in an array or object. + * @return self + * @throws \InvalidArgumentException if an unsupported argument type is + * provided. + */ + public function withParsedBody($data) + { + if (!is_null($data) && !is_object($data) && !is_array($data)) { + throw new InvalidArgumentException('Parsed body value must be an array, an object, or null'); + } + + $clone = clone $this; + $clone->bodyParsed = $data; + + return $clone; + } + + /** + * Force Body to be parsed again. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return self + */ + public function reparseBody() + { + $this->bodyParsed = false; + + return $this; + } + + /** + * Register media type parser. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $mediaType A HTTP media type (excluding content-type + * params). + * @param callable $callable A callable that returns parsed contents for + * media type. + */ + public function registerMediaTypeParser($mediaType, callable $callable) + { + if ($callable instanceof Closure) { + $callable = $callable->bindTo($this); + } + $this->bodyParsers[(string)$mediaType] = $callable; + } + + /******************************************************************************* + * Parameters (e.g., POST and GET data) + ******************************************************************************/ + + /** + * Fetch request parameter value from body or query string (in that order). + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $key The parameter key. + * @param string $default The default value. + * + * @return mixed The parameter value. + */ + public function getParam($key, $default = null) + { + $postParams = $this->getParsedBody(); + $getParams = $this->getQueryParams(); + $result = $default; + if (is_array($postParams) && isset($postParams[$key])) { + $result = $postParams[$key]; + } elseif (is_object($postParams) && property_exists($postParams, $key)) { + $result = $postParams->$key; + } elseif (isset($getParams[$key])) { + $result = $getParams[$key]; + } + + return $result; + } + + /** + * Fetch parameter value from request body. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param $key + * @param null $default + * + * @return null + */ + public function getParsedBodyParam($key, $default = null) + { + $postParams = $this->getParsedBody(); + $result = $default; + if (is_array($postParams) && isset($postParams[$key])) { + $result = $postParams[$key]; + } elseif (is_object($postParams) && property_exists($postParams, $key)) { + $result = $postParams->$key; + } + + return $result; + } + + /** + * Fetch parameter value from query string. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param $key + * @param null $default + * + * @return null + */ + public function getQueryParam($key, $default = null) + { + $getParams = $this->getQueryParams(); + $result = $default; + if (isset($getParams[$key])) { + $result = $getParams[$key]; + } + + return $result; + } + + /** + * Fetch assocative array of body and query string parameters. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return array + */ + public function getParams() + { + $params = $this->getQueryParams(); + $postParams = $this->getParsedBody(); + if ($postParams) { + $params = array_merge($params, (array)$postParams); + } + + return $params; + } +} diff --git a/vendor/slim/slim/Slim/Http/RequestBody.php b/vendor/slim/slim/Slim/Http/RequestBody.php new file mode 100644 index 0000000..2345fe4 --- /dev/null +++ b/vendor/slim/slim/Slim/Http/RequestBody.php @@ -0,0 +1,27 @@ + 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + //Successful 2xx + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 208 => 'Already Reported', + 226 => 'IM Used', + //Redirection 3xx + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => '(Unused)', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + //Client Error 4xx + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 451 => 'Unavailable For Legal Reasons', + //Server Error 5xx + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 510 => 'Not Extended', + 511 => 'Network Authentication Required', + ]; + + /** + * Create new HTTP response. + * + * @param int $status The response status code. + * @param HeadersInterface|null $headers The response headers. + * @param StreamInterface|null $body The response body. + */ + public function __construct($status = 200, HeadersInterface $headers = null, StreamInterface $body = null) + { + $this->status = $this->filterStatus($status); + $this->headers = $headers ? $headers : new Headers(); + $this->body = $body ? $body : new Body(fopen('php://temp', 'r+')); + } + + /** + * This method is applied to the cloned object + * after PHP performs an initial shallow-copy. This + * method completes a deep-copy by creating new objects + * for the cloned object's internal reference pointers. + */ + public function __clone() + { + $this->headers = clone $this->headers; + $this->body = clone $this->body; + } + + /******************************************************************************* + * Status + ******************************************************************************/ + + /** + * Gets the response status code. + * + * The status code is a 3-digit integer result code of the server's attempt + * to understand and satisfy the request. + * + * @return int Status code. + */ + public function getStatusCode() + { + return $this->status; + } + + /** + * Return an instance with the specified status code and, optionally, reason phrase. + * + * If no reason phrase is specified, implementations MAY choose to default + * to the RFC 7231 or IANA recommended reason phrase for the response's + * status code. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated status and reason phrase. + * + * @link http://tools.ietf.org/html/rfc7231#section-6 + * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * @param int $code The 3-digit integer result code to set. + * @param string $reasonPhrase The reason phrase to use with the + * provided status code; if none is provided, implementations MAY + * use the defaults as suggested in the HTTP specification. + * @return self + * @throws \InvalidArgumentException For invalid status code arguments. + */ + public function withStatus($code, $reasonPhrase = '') + { + $code = $this->filterStatus($code); + + if (!is_string($reasonPhrase) && !method_exists($reasonPhrase, '__toString')) { + throw new InvalidArgumentException('ReasonPhrase must be a string'); + } + + $clone = clone $this; + $clone->status = $code; + if ($reasonPhrase === '' && isset(static::$messages[$code])) { + $reasonPhrase = static::$messages[$code]; + } + + if ($reasonPhrase === '') { + throw new InvalidArgumentException('ReasonPhrase must be supplied for this code'); + } + + $clone->reasonPhrase = $reasonPhrase; + + return $clone; + } + + /** + * Filter HTTP status code. + * + * @param int $status HTTP status code. + * @return int + * @throws \InvalidArgumentException If an invalid HTTP status code is provided. + */ + protected function filterStatus($status) + { + if (!is_integer($status) || $status<100 || $status>599) { + throw new InvalidArgumentException('Invalid HTTP status code'); + } + + return $status; + } + + /** + * Gets the response reason phrase associated with the status code. + * + * Because a reason phrase is not a required element in a response + * status line, the reason phrase value MAY be null. Implementations MAY + * choose to return the default RFC 7231 recommended reason phrase (or those + * listed in the IANA HTTP Status Code Registry) for the response's + * status code. + * + * @link http://tools.ietf.org/html/rfc7231#section-6 + * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * @return string Reason phrase; must return an empty string if none present. + */ + public function getReasonPhrase() + { + if ($this->reasonPhrase) { + return $this->reasonPhrase; + } + if (isset(static::$messages[$this->status])) { + return static::$messages[$this->status]; + } + return ''; + } + + /******************************************************************************* + * Body + ******************************************************************************/ + + /** + * Write data to the response body. + * + * Note: This method is not part of the PSR-7 standard. + * + * Proxies to the underlying stream and writes the provided data to it. + * + * @param string $data + * @return self + */ + public function write($data) + { + $this->getBody()->write($data); + + return $this; + } + + /******************************************************************************* + * Response Helpers + ******************************************************************************/ + + /** + * Redirect. + * + * Note: This method is not part of the PSR-7 standard. + * + * This method prepares the response object to return an HTTP Redirect + * response to the client. + * + * @param string|UriInterface $url The redirect destination. + * @param int $status The redirect HTTP status code. + * @return self + */ + public function withRedirect($url, $status = 302) + { + return $this->withStatus($status)->withHeader('Location', (string)$url); + } + + /** + * Json. + * + * Note: This method is not part of the PSR-7 standard. + * + * This method prepares the response object to return an HTTP Json + * response to the client. + * + * @param mixed $data The data + * @param int $status The HTTP status code. + * @param int $encodingOptions Json encoding options + * @throws \RuntimeException + * @return self + */ + public function withJson($data, $status = null, $encodingOptions = 0) + { + $body = $this->getBody(); + $body->rewind(); + $body->write($json = json_encode($data, $encodingOptions)); + + // Ensure that the json encoding passed successfully + if ($json === false) { + throw new \RuntimeException(json_last_error_msg(), json_last_error()); + } + + $responseWithJson = $this->withHeader('Content-Type', 'application/json;charset=utf-8'); + if (isset($status)) { + return $responseWithJson->withStatus($status); + } + return $responseWithJson; + } + + /** + * Is this response empty? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isEmpty() + { + return in_array($this->getStatusCode(), [204, 205, 304]); + } + + /** + * Is this response informational? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isInformational() + { + return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200; + } + + /** + * Is this response OK? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isOk() + { + return $this->getStatusCode() === 200; + } + + /** + * Is this response successful? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isSuccessful() + { + return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300; + } + + /** + * Is this response a redirect? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isRedirect() + { + return in_array($this->getStatusCode(), [301, 302, 303, 307]); + } + + /** + * Is this response a redirection? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isRedirection() + { + return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400; + } + + /** + * Is this response forbidden? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + * @api + */ + public function isForbidden() + { + return $this->getStatusCode() === 403; + } + + /** + * Is this response not Found? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isNotFound() + { + return $this->getStatusCode() === 404; + } + + /** + * Is this response a client error? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isClientError() + { + return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500; + } + + /** + * Is this response a server error? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isServerError() + { + return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600; + } + + /** + * Convert response to string. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string + */ + public function __toString() + { + $output = sprintf( + 'HTTP/%s %s %s', + $this->getProtocolVersion(), + $this->getStatusCode(), + $this->getReasonPhrase() + ); + $output .= PHP_EOL; + foreach ($this->getHeaders() as $name => $values) { + $output .= sprintf('%s: %s', $name, $this->getHeaderLine($name)) . PHP_EOL; + } + $output .= PHP_EOL; + $output .= (string)$this->getBody(); + + return $output; + } +} diff --git a/vendor/slim/slim/Slim/Http/Stream.php b/vendor/slim/slim/Slim/Http/Stream.php new file mode 100644 index 0000000..97de9ac --- /dev/null +++ b/vendor/slim/slim/Slim/Http/Stream.php @@ -0,0 +1,409 @@ + ['r', 'r+', 'w+', 'a+', 'x+', 'c+'], + 'writable' => ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'], + ]; + + /** + * The underlying stream resource + * + * @var resource + */ + protected $stream; + + /** + * Stream metadata + * + * @var array + */ + protected $meta; + + /** + * Is this stream readable? + * + * @var bool + */ + protected $readable; + + /** + * Is this stream writable? + * + * @var bool + */ + protected $writable; + + /** + * Is this stream seekable? + * + * @var bool + */ + protected $seekable; + + /** + * The size of the stream if known + * + * @var null|int + */ + protected $size; + + /** + * Create a new Stream. + * + * @param resource $stream A PHP resource handle. + * + * @throws InvalidArgumentException If argument is not a resource. + */ + public function __construct($stream) + { + $this->attach($stream); + } + + /** + * Get stream metadata as an associative array or retrieve a specific key. + * + * The keys returned are identical to the keys returned from PHP's + * stream_get_meta_data() function. + * + * @link http://php.net/manual/en/function.stream-get-meta-data.php + * + * @param string $key Specific metadata to retrieve. + * + * @return array|mixed|null Returns an associative array if no key is + * provided. Returns a specific key value if a key is provided and the + * value is found, or null if the key is not found. + */ + public function getMetadata($key = null) + { + $this->meta = stream_get_meta_data($this->stream); + if (is_null($key) === true) { + return $this->meta; + } + + return isset($this->meta[$key]) ? $this->meta[$key] : null; + } + + /** + * Is a resource attached to this stream? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + protected function isAttached() + { + return is_resource($this->stream); + } + + /** + * Attach new resource to this object. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param resource $newStream A PHP resource handle. + * + * @throws InvalidArgumentException If argument is not a valid PHP resource. + */ + protected function attach($newStream) + { + if (is_resource($newStream) === false) { + throw new InvalidArgumentException(__METHOD__ . ' argument must be a valid PHP resource'); + } + + if ($this->isAttached() === true) { + $this->detach(); + } + + $this->stream = $newStream; + } + + /** + * Separates any underlying resources from the stream. + * + * After the stream has been detached, the stream is in an unusable state. + * + * @return resource|null Underlying PHP stream, if any + */ + public function detach() + { + $oldResource = $this->stream; + $this->stream = null; + $this->meta = null; + $this->readable = null; + $this->writable = null; + $this->seekable = null; + $this->size = null; + + return $oldResource; + } + + /** + * Reads all data from the stream into a string, from the beginning to end. + * + * This method MUST attempt to seek to the beginning of the stream before + * reading data and read the stream until the end is reached. + * + * Warning: This could attempt to load a large amount of data into memory. + * + * This method MUST NOT raise an exception in order to conform with PHP's + * string casting operations. + * + * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring + * @return string + */ + public function __toString() + { + if (!$this->isAttached()) { + return ''; + } + + try { + $this->rewind(); + return $this->getContents(); + } catch (RuntimeException $e) { + return ''; + } + } + + /** + * Closes the stream and any underlying resources. + */ + public function close() + { + if ($this->isAttached() === true) { + fclose($this->stream); + } + + $this->detach(); + } + + /** + * Get the size of the stream if known. + * + * @return int|null Returns the size in bytes if known, or null if unknown. + */ + public function getSize() + { + if (!$this->size && $this->isAttached() === true) { + $stats = fstat($this->stream); + $this->size = isset($stats['size']) ? $stats['size'] : null; + } + + return $this->size; + } + + /** + * Returns the current position of the file read/write pointer + * + * @return int Position of the file pointer + * + * @throws RuntimeException on error. + */ + public function tell() + { + if (!$this->isAttached() || ($position = ftell($this->stream)) === false) { + throw new RuntimeException('Could not get the position of the pointer in stream'); + } + + return $position; + } + + /** + * Returns true if the stream is at the end of the stream. + * + * @return bool + */ + public function eof() + { + return $this->isAttached() ? feof($this->stream) : true; + } + + /** + * Returns whether or not the stream is readable. + * + * @return bool + */ + public function isReadable() + { + if ($this->readable === null) { + $this->readable = false; + if ($this->isAttached()) { + $meta = $this->getMetadata(); + foreach (self::$modes['readable'] as $mode) { + if (strpos($meta['mode'], $mode) === 0) { + $this->readable = true; + break; + } + } + } + } + + return $this->readable; + } + + /** + * Returns whether or not the stream is writable. + * + * @return bool + */ + public function isWritable() + { + if ($this->writable === null) { + $this->writable = false; + if ($this->isAttached()) { + $meta = $this->getMetadata(); + foreach (self::$modes['writable'] as $mode) { + if (strpos($meta['mode'], $mode) === 0) { + $this->writable = true; + break; + } + } + } + } + + return $this->writable; + } + + /** + * Returns whether or not the stream is seekable. + * + * @return bool + */ + public function isSeekable() + { + if ($this->seekable === null) { + $this->seekable = false; + if ($this->isAttached()) { + $meta = $this->getMetadata(); + $this->seekable = $meta['seekable']; + } + } + + return $this->seekable; + } + + /** + * Seek to a position in the stream. + * + * @link http://www.php.net/manual/en/function.fseek.php + * + * @param int $offset Stream offset + * @param int $whence Specifies how the cursor position will be calculated + * based on the seek offset. Valid values are identical to the built-in + * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to + * offset bytes SEEK_CUR: Set position to current location plus offset + * SEEK_END: Set position to end-of-stream plus offset. + * + * @throws RuntimeException on failure. + */ + public function seek($offset, $whence = SEEK_SET) + { + // Note that fseek returns 0 on success! + if (!$this->isSeekable() || fseek($this->stream, $offset, $whence) === -1) { + throw new RuntimeException('Could not seek in stream'); + } + } + + /** + * Seek to the beginning of the stream. + * + * If the stream is not seekable, this method will raise an exception; + * otherwise, it will perform a seek(0). + * + * @see seek() + * + * @link http://www.php.net/manual/en/function.fseek.php + * + * @throws RuntimeException on failure. + */ + public function rewind() + { + if (!$this->isSeekable() || rewind($this->stream) === false) { + throw new RuntimeException('Could not rewind stream'); + } + } + + /** + * Read data from the stream. + * + * @param int $length Read up to $length bytes from the object and return + * them. Fewer than $length bytes may be returned if underlying stream + * call returns fewer bytes. + * + * @return string Returns the data read from the stream, or an empty string + * if no bytes are available. + * + * @throws RuntimeException if an error occurs. + */ + public function read($length) + { + if (!$this->isReadable() || ($data = fread($this->stream, $length)) === false) { + throw new RuntimeException('Could not read from stream'); + } + + return $data; + } + + /** + * Write data to the stream. + * + * @param string $string The string that is to be written. + * + * @return int Returns the number of bytes written to the stream. + * + * @throws RuntimeException on failure. + */ + public function write($string) + { + if (!$this->isWritable() || ($written = fwrite($this->stream, $string)) === false) { + throw new RuntimeException('Could not write to stream'); + } + + // reset size so that it will be recalculated on next call to getSize() + $this->size = null; + + return $written; + } + + /** + * Returns the remaining contents in a string + * + * @return string + * + * @throws RuntimeException if unable to read or an error occurs while + * reading. + */ + public function getContents() + { + if (!$this->isReadable() || ($contents = stream_get_contents($this->stream)) === false) { + throw new RuntimeException('Could not get contents of stream'); + } + + return $contents; + } +} diff --git a/vendor/slim/slim/Slim/Http/UploadedFile.php b/vendor/slim/slim/Slim/Http/UploadedFile.php new file mode 100644 index 0000000..ff97027 --- /dev/null +++ b/vendor/slim/slim/Slim/Http/UploadedFile.php @@ -0,0 +1,327 @@ +has('slim.files')) { + return $env['slim.files']; + } elseif (isset($_FILES)) { + return static::parseUploadedFiles($_FILES); + } + + return []; + } + + /** + * Parse a non-normalized, i.e. $_FILES superglobal, tree of uploaded file data. + * + * @param array $uploadedFiles The non-normalized tree of uploaded file data. + * + * @return array A normalized tree of UploadedFile instances. + */ + private static function parseUploadedFiles(array $uploadedFiles) + { + $parsed = []; + foreach ($uploadedFiles as $field => $uploadedFile) { + if (!isset($uploadedFile['error'])) { + if (is_array($uploadedFile)) { + $parsed[$field] = static::parseUploadedFiles($uploadedFile); + } + continue; + } + + $parsed[$field] = []; + if (!is_array($uploadedFile['error'])) { + $parsed[$field] = new static( + $uploadedFile['tmp_name'], + isset($uploadedFile['name']) ? $uploadedFile['name'] : null, + isset($uploadedFile['type']) ? $uploadedFile['type'] : null, + isset($uploadedFile['size']) ? $uploadedFile['size'] : null, + $uploadedFile['error'], + true + ); + } else { + $subArray = []; + foreach ($uploadedFile['error'] as $fileIdx => $error) { + // normalise subarray and re-parse to move the input's keyname up a level + $subArray[$fileIdx]['name'] = $uploadedFile['name'][$fileIdx]; + $subArray[$fileIdx]['type'] = $uploadedFile['type'][$fileIdx]; + $subArray[$fileIdx]['tmp_name'] = $uploadedFile['tmp_name'][$fileIdx]; + $subArray[$fileIdx]['error'] = $uploadedFile['error'][$fileIdx]; + $subArray[$fileIdx]['size'] = $uploadedFile['size'][$fileIdx]; + + $parsed[$field] = static::parseUploadedFiles($subArray); + } + } + } + + return $parsed; + } + + /** + * Construct a new UploadedFile instance. + * + * @param string $file The full path to the uploaded file provided by the client. + * @param string|null $name The file name. + * @param string|null $type The file media type. + * @param int|null $size The file size in bytes. + * @param int $error The UPLOAD_ERR_XXX code representing the status of the upload. + * @param bool $sapi Indicates if the upload is in a SAPI environment. + */ + public function __construct($file, $name = null, $type = null, $size = null, $error = UPLOAD_ERR_OK, $sapi = false) + { + $this->file = $file; + $this->name = $name; + $this->type = $type; + $this->size = $size; + $this->error = $error; + $this->sapi = $sapi; + } + + /** + * Retrieve a stream representing the uploaded file. + * + * This method MUST return a StreamInterface instance, representing the + * uploaded file. The purpose of this method is to allow utilizing native PHP + * stream functionality to manipulate the file upload, such as + * stream_copy_to_stream() (though the result will need to be decorated in a + * native PHP stream wrapper to work with such functions). + * + * If the moveTo() method has been called previously, this method MUST raise + * an exception. + * + * @return StreamInterface Stream representation of the uploaded file. + * @throws \RuntimeException in cases when no stream is available or can be + * created. + */ + public function getStream() + { + if ($this->moved) { + throw new \RuntimeException(sprintf('Uploaded file %1s has already been moved', $this->name)); + } + if ($this->stream === null) { + $this->stream = new Stream(fopen($this->file, 'r')); + } + + return $this->stream; + } + + /** + * Move the uploaded file to a new location. + * + * Use this method as an alternative to move_uploaded_file(). This method is + * guaranteed to work in both SAPI and non-SAPI environments. + * Implementations must determine which environment they are in, and use the + * appropriate method (move_uploaded_file(), rename(), or a stream + * operation) to perform the operation. + * + * $targetPath may be an absolute path, or a relative path. If it is a + * relative path, resolution should be the same as used by PHP's rename() + * function. + * + * The original file or stream MUST be removed on completion. + * + * If this method is called more than once, any subsequent calls MUST raise + * an exception. + * + * When used in an SAPI environment where $_FILES is populated, when writing + * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be + * used to ensure permissions and upload status are verified correctly. + * + * If you wish to move to a stream, use getStream(), as SAPI operations + * cannot guarantee writing to stream destinations. + * + * @see http://php.net/is_uploaded_file + * @see http://php.net/move_uploaded_file + * + * @param string $targetPath Path to which to move the uploaded file. + * + * @throws InvalidArgumentException if the $path specified is invalid. + * @throws RuntimeException on any error during the move operation, or on + * the second or subsequent call to the method. + */ + public function moveTo($targetPath) + { + if ($this->moved) { + throw new RuntimeException('Uploaded file already moved'); + } + + if (!is_writable(dirname($targetPath))) { + throw new InvalidArgumentException('Upload target path is not writable'); + } + + $targetIsStream = strpos($targetPath, '://') > 0; + if ($targetIsStream) { + if (!copy($this->file, $targetPath)) { + throw new RuntimeException(sprintf('Error moving uploaded file %1s to %2s', $this->name, $targetPath)); + } + if (!unlink($this->file)) { + throw new RuntimeException(sprintf('Error removing uploaded file %1s', $this->name)); + } + } elseif ($this->sapi) { + if (!is_uploaded_file($this->file)) { + throw new RuntimeException(sprintf('%1s is not a valid uploaded file', $this->file)); + } + + if (!move_uploaded_file($this->file, $targetPath)) { + throw new RuntimeException(sprintf('Error moving uploaded file %1s to %2s', $this->name, $targetPath)); + } + } else { + if (!rename($this->file, $targetPath)) { + throw new RuntimeException(sprintf('Error moving uploaded file %1s to %2s', $this->name, $targetPath)); + } + } + + $this->moved = true; + } + + /** + * Retrieve the error associated with the uploaded file. + * + * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants. + * + * If the file was uploaded successfully, this method MUST return + * UPLOAD_ERR_OK. + * + * Implementations SHOULD return the value stored in the "error" key of + * the file in the $_FILES array. + * + * @see http://php.net/manual/en/features.file-upload.errors.php + * + * @return int One of PHP's UPLOAD_ERR_XXX constants. + */ + public function getError() + { + return $this->error; + } + + /** + * Retrieve the filename sent by the client. + * + * Do not trust the value returned by this method. A client could send + * a malicious filename with the intention to corrupt or hack your + * application. + * + * Implementations SHOULD return the value stored in the "name" key of + * the file in the $_FILES array. + * + * @return string|null The filename sent by the client or null if none + * was provided. + */ + public function getClientFilename() + { + return $this->name; + } + + /** + * Retrieve the media type sent by the client. + * + * Do not trust the value returned by this method. A client could send + * a malicious media type with the intention to corrupt or hack your + * application. + * + * Implementations SHOULD return the value stored in the "type" key of + * the file in the $_FILES array. + * + * @return string|null The media type sent by the client or null if none + * was provided. + */ + public function getClientMediaType() + { + return $this->type; + } + + /** + * Retrieve the file size. + * + * Implementations SHOULD return the value stored in the "size" key of + * the file in the $_FILES array if available, as PHP calculates this based + * on the actual size transmitted. + * + * @return int|null The file size in bytes or null if unknown. + */ + public function getSize() + { + return $this->size; + } +} diff --git a/vendor/slim/slim/Slim/Http/Uri.php b/vendor/slim/slim/Slim/Http/Uri.php new file mode 100644 index 0000000..27b1d0d --- /dev/null +++ b/vendor/slim/slim/Slim/Http/Uri.php @@ -0,0 +1,821 @@ +scheme = $this->filterScheme($scheme); + $this->host = $host; + $this->port = $this->filterPort($port); + $this->path = empty($path) ? '/' : $this->filterPath($path); + $this->query = $this->filterQuery($query); + $this->fragment = $this->filterQuery($fragment); + $this->user = $user; + $this->password = $password; + } + + /** + * Create new Uri from string. + * + * @param string $uri Complete Uri string + * (i.e., https://user:pass@host:443/path?query). + * + * @return self + */ + public static function createFromString($uri) + { + if (!is_string($uri) && !method_exists($uri, '__toString')) { + throw new InvalidArgumentException('Uri must be a string'); + } + + $parts = parse_url($uri); + $scheme = isset($parts['scheme']) ? $parts['scheme'] : ''; + $user = isset($parts['user']) ? $parts['user'] : ''; + $pass = isset($parts['pass']) ? $parts['pass'] : ''; + $host = isset($parts['host']) ? $parts['host'] : ''; + $port = isset($parts['port']) ? $parts['port'] : null; + $path = isset($parts['path']) ? $parts['path'] : ''; + $query = isset($parts['query']) ? $parts['query'] : ''; + $fragment = isset($parts['fragment']) ? $parts['fragment'] : ''; + + return new static($scheme, $host, $port, $path, $query, $fragment, $user, $pass); + } + + /** + * Create new Uri from environment. + * + * @param Environment $env + * + * @return self + */ + public static function createFromEnvironment(Environment $env) + { + // Scheme + $isSecure = $env->get('HTTPS'); + $scheme = (empty($isSecure) || $isSecure === 'off') ? 'http' : 'https'; + + // Authority: Username and password + $username = $env->get('PHP_AUTH_USER', ''); + $password = $env->get('PHP_AUTH_PW', ''); + + // Authority: Host + if ($env->has('HTTP_HOST')) { + $host = $env->get('HTTP_HOST'); + } else { + $host = $env->get('SERVER_NAME'); + } + + // Authority: Port + $port = (int)$env->get('SERVER_PORT', 80); + if (preg_match('/^(\[[a-fA-F0-9:.]+\])(:\d+)?\z/', $host, $matches)) { + $host = $matches[1]; + + if ($matches[2]) { + $port = (int) substr($matches[2], 1); + } + } else { + $pos = strpos($host, ':'); + if ($pos !== false) { + $port = (int) substr($host, $pos + 1); + $host = strstr($host, ':', true); + } + } + + // Path + $requestScriptName = parse_url($env->get('SCRIPT_NAME'), PHP_URL_PATH); + $requestScriptDir = dirname($requestScriptName); + + // parse_url() requires a full URL. As we don't extract the domain name or scheme, + // we use a stand-in. + $requestUri = parse_url('http://example.com' . $env->get('REQUEST_URI'), PHP_URL_PATH); + + $basePath = ''; + $virtualPath = $requestUri; + if (stripos($requestUri, $requestScriptName) === 0) { + $basePath = $requestScriptName; + } elseif ($requestScriptDir !== '/' && stripos($requestUri, $requestScriptDir) === 0) { + $basePath = $requestScriptDir; + } + + if ($basePath) { + $virtualPath = ltrim(substr($requestUri, strlen($basePath)), '/'); + } + + // Query string + $queryString = $env->get('QUERY_STRING', ''); + + // Fragment + $fragment = ''; + + // Build Uri + $uri = new static($scheme, $host, $port, $virtualPath, $queryString, $fragment, $username, $password); + if ($basePath) { + $uri = $uri->withBasePath($basePath); + } + + return $uri; + } + + /******************************************************************************** + * Scheme + *******************************************************************************/ + + /** + * Retrieve the scheme component of the URI. + * + * If no scheme is present, this method MUST return an empty string. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.1. + * + * The trailing ":" character is not part of the scheme and MUST NOT be + * added. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.1 + * @return string The URI scheme. + */ + public function getScheme() + { + return $this->scheme; + } + + /** + * Return an instance with the specified scheme. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified scheme. + * + * Implementations MUST support the schemes "http" and "https" case + * insensitively, and MAY accommodate other schemes if required. + * + * An empty scheme is equivalent to removing the scheme. + * + * @param string $scheme The scheme to use with the new instance. + * @return self A new instance with the specified scheme. + * @throws \InvalidArgumentException for invalid or unsupported schemes. + */ + public function withScheme($scheme) + { + $scheme = $this->filterScheme($scheme); + $clone = clone $this; + $clone->scheme = $scheme; + + return $clone; + } + + /** + * Filter Uri scheme. + * + * @param string $scheme Raw Uri scheme. + * @return string + * + * @throws InvalidArgumentException If the Uri scheme is not a string. + * @throws InvalidArgumentException If Uri scheme is not "", "https", or "http". + */ + protected function filterScheme($scheme) + { + static $valid = [ + '' => true, + 'https' => true, + 'http' => true, + ]; + + if (!is_string($scheme) && !method_exists($scheme, '__toString')) { + throw new InvalidArgumentException('Uri scheme must be a string'); + } + + $scheme = str_replace('://', '', strtolower((string)$scheme)); + if (!isset($valid[$scheme])) { + throw new InvalidArgumentException('Uri scheme must be one of: "", "https", "http"'); + } + + return $scheme; + } + + /******************************************************************************** + * Authority + *******************************************************************************/ + + /** + * Retrieve the authority component of the URI. + * + * If no authority information is present, this method MUST return an empty + * string. + * + * The authority syntax of the URI is: + * + *
+     * [user-info@]host[:port]
+     * 
+ * + * If the port component is not set or is the standard port for the current + * scheme, it SHOULD NOT be included. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2 + * @return string The URI authority, in "[user-info@]host[:port]" format. + */ + public function getAuthority() + { + $userInfo = $this->getUserInfo(); + $host = $this->getHost(); + $port = $this->getPort(); + + return ($userInfo ? $userInfo . '@' : '') . $host . ($port !== null ? ':' . $port : ''); + } + + /** + * Retrieve the user information component of the URI. + * + * If no user information is present, this method MUST return an empty + * string. + * + * If a user is present in the URI, this will return that value; + * additionally, if the password is also present, it will be appended to the + * user value, with a colon (":") separating the values. + * + * The trailing "@" character is not part of the user information and MUST + * NOT be added. + * + * @return string The URI user information, in "username[:password]" format. + */ + public function getUserInfo() + { + return $this->user . ($this->password ? ':' . $this->password : ''); + } + + /** + * Return an instance with the specified user information. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified user information. + * + * Password is optional, but the user information MUST include the + * user; an empty string for the user is equivalent to removing user + * information. + * + * @param string $user The user name to use for authority. + * @param null|string $password The password associated with $user. + * @return self A new instance with the specified user information. + */ + public function withUserInfo($user, $password = null) + { + $clone = clone $this; + $clone->user = $user; + $clone->password = $password ? $password : ''; + + return $clone; + } + + /** + * Retrieve the host component of the URI. + * + * If no host is present, this method MUST return an empty string. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.2.2. + * + * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 + * @return string The URI host. + */ + public function getHost() + { + return $this->host; + } + + /** + * Return an instance with the specified host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified host. + * + * An empty host value is equivalent to removing the host. + * + * @param string $host The hostname to use with the new instance. + * @return self A new instance with the specified host. + * @throws \InvalidArgumentException for invalid hostnames. + */ + public function withHost($host) + { + $clone = clone $this; + $clone->host = $host; + + return $clone; + } + + /** + * Retrieve the port component of the URI. + * + * If a port is present, and it is non-standard for the current scheme, + * this method MUST return it as an integer. If the port is the standard port + * used with the current scheme, this method SHOULD return null. + * + * If no port is present, and no scheme is present, this method MUST return + * a null value. + * + * If no port is present, but a scheme is present, this method MAY return + * the standard port for that scheme, but SHOULD return null. + * + * @return null|int The URI port. + */ + public function getPort() + { + return $this->port && !$this->hasStandardPort() ? $this->port : null; + } + + /** + * Return an instance with the specified port. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified port. + * + * Implementations MUST raise an exception for ports outside the + * established TCP and UDP port ranges. + * + * A null value provided for the port is equivalent to removing the port + * information. + * + * @param null|int $port The port to use with the new instance; a null value + * removes the port information. + * @return self A new instance with the specified port. + * @throws \InvalidArgumentException for invalid ports. + */ + public function withPort($port) + { + $port = $this->filterPort($port); + $clone = clone $this; + $clone->port = $port; + + return $clone; + } + + /** + * Does this Uri use a standard port? + * + * @return bool + */ + protected function hasStandardPort() + { + return ($this->scheme === 'http' && $this->port === 80) || ($this->scheme === 'https' && $this->port === 443); + } + + /** + * Filter Uri port. + * + * @param null|int $port The Uri port number. + * @return null|int + * + * @throws InvalidArgumentException If the port is invalid. + */ + protected function filterPort($port) + { + if (is_null($port) || (is_integer($port) && ($port >= 1 && $port <= 65535))) { + return $port; + } + + throw new InvalidArgumentException('Uri port must be null or an integer between 1 and 65535 (inclusive)'); + } + + /******************************************************************************** + * Path + *******************************************************************************/ + + /** + * Retrieve the path component of the URI. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * Normally, the empty path "" and absolute path "/" are considered equal as + * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically + * do this normalization because in contexts with a trimmed base path, e.g. + * the front controller, this difference becomes significant. It's the task + * of the user to handle both "" and "/". + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.3. + * + * As an example, if the value should include a slash ("/") not intended as + * delimiter between path segments, that value MUST be passed in encoded + * form (e.g., "%2F") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.3 + * @return string The URI path. + */ + public function getPath() + { + return $this->path; + } + + /** + * Return an instance with the specified path. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified path. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * If the path is intended to be domain-relative rather than path relative then + * it must begin with a slash ("/"). Paths not starting with a slash ("/") + * are assumed to be relative to some base path known to the application or + * consumer. + * + * Users can provide both encoded and decoded path characters. + * Implementations ensure the correct encoding as outlined in getPath(). + * + * @param string $path The path to use with the new instance. + * @return self A new instance with the specified path. + * @throws \InvalidArgumentException for invalid paths. + */ + public function withPath($path) + { + if (!is_string($path)) { + throw new InvalidArgumentException('Uri path must be a string'); + } + + $clone = clone $this; + $clone->path = $this->filterPath($path); + + // if the path is absolute, then clear basePath + if (substr($path, 0, 1) == '/') { + $clone->basePath = ''; + } + + return $clone; + } + + /** + * Retrieve the base path segment of the URI. + * + * Note: This method is not part of the PSR-7 standard. + * + * This method MUST return a string; if no path is present it MUST return + * an empty string. + * + * @return string The base path segment of the URI. + */ + public function getBasePath() + { + return $this->basePath; + } + + /** + * Set base path. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $basePath + * @return self + */ + public function withBasePath($basePath) + { + if (!is_string($basePath)) { + throw new InvalidArgumentException('Uri path must be a string'); + } + if (!empty($basePath)) { + $basePath = '/' . trim($basePath, '/'); // <-- Trim on both sides + } + $clone = clone $this; + + if ($basePath !== '/') { + $clone->basePath = $this->filterPath($basePath); + } + + return $clone; + } + + /** + * Filter Uri path. + * + * This method percent-encodes all reserved + * characters in the provided path string. This method + * will NOT double-encode characters that are already + * percent-encoded. + * + * @param string $path The raw uri path. + * @return string The RFC 3986 percent-encoded uri path. + * @link http://www.faqs.org/rfcs/rfc3986.html + */ + protected function filterPath($path) + { + return preg_replace_callback( + '/(?:[^a-zA-Z0-9_\-\.~:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/', + function ($match) { + return rawurlencode($match[0]); + }, + $path + ); + } + + /******************************************************************************** + * Query + *******************************************************************************/ + + /** + * Retrieve the query string of the URI. + * + * If no query string is present, this method MUST return an empty string. + * + * The leading "?" character is not part of the query and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.4. + * + * As an example, if a value in a key/value pair of the query string should + * include an ampersand ("&") not intended as a delimiter between values, + * that value MUST be passed in encoded form (e.g., "%26") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.4 + * @return string The URI query string. + */ + public function getQuery() + { + return $this->query; + } + + /** + * Return an instance with the specified query string. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified query string. + * + * Users can provide both encoded and decoded query characters. + * Implementations ensure the correct encoding as outlined in getQuery(). + * + * An empty query string value is equivalent to removing the query string. + * + * @param string $query The query string to use with the new instance. + * @return self A new instance with the specified query string. + * @throws \InvalidArgumentException for invalid query strings. + */ + public function withQuery($query) + { + if (!is_string($query) && !method_exists($query, '__toString')) { + throw new InvalidArgumentException('Uri query must be a string'); + } + $query = ltrim((string)$query, '?'); + $clone = clone $this; + $clone->query = $this->filterQuery($query); + + return $clone; + } + + /** + * Filters the query string or fragment of a URI. + * + * @param string $query The raw uri query string. + * @return string The percent-encoded query string. + */ + protected function filterQuery($query) + { + return preg_replace_callback( + '/(?:[^a-zA-Z0-9_\-\.~!\$&\'\(\)\*\+,;=%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/', + function ($match) { + return rawurlencode($match[0]); + }, + $query + ); + } + + /******************************************************************************** + * Fragment + *******************************************************************************/ + + /** + * Retrieve the fragment component of the URI. + * + * If no fragment is present, this method MUST return an empty string. + * + * The leading "#" character is not part of the fragment and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.5. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.5 + * @return string The URI fragment. + */ + public function getFragment() + { + return $this->fragment; + } + + /** + * Return an instance with the specified URI fragment. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified URI fragment. + * + * Users can provide both encoded and decoded fragment characters. + * Implementations ensure the correct encoding as outlined in getFragment(). + * + * An empty fragment value is equivalent to removing the fragment. + * + * @param string $fragment The fragment to use with the new instance. + * @return self A new instance with the specified fragment. + */ + public function withFragment($fragment) + { + if (!is_string($fragment) && !method_exists($fragment, '__toString')) { + throw new InvalidArgumentException('Uri fragment must be a string'); + } + $fragment = ltrim((string)$fragment, '#'); + $clone = clone $this; + $clone->fragment = $this->filterQuery($fragment); + + return $clone; + } + + /******************************************************************************** + * Helpers + *******************************************************************************/ + + /** + * Return the string representation as a URI reference. + * + * Depending on which components of the URI are present, the resulting + * string is either a full URI or relative reference according to RFC 3986, + * Section 4.1. The method concatenates the various components of the URI, + * using the appropriate delimiters: + * + * - If a scheme is present, it MUST be suffixed by ":". + * - If an authority is present, it MUST be prefixed by "//". + * - The path can be concatenated without delimiters. But there are two + * cases where the path has to be adjusted to make the URI reference + * valid as PHP does not allow to throw an exception in __toString(): + * - If the path is rootless and an authority is present, the path MUST + * be prefixed by "/". + * - If the path is starting with more than one "/" and no authority is + * present, the starting slashes MUST be reduced to one. + * - If a query is present, it MUST be prefixed by "?". + * - If a fragment is present, it MUST be prefixed by "#". + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + * @return string + */ + public function __toString() + { + $scheme = $this->getScheme(); + $authority = $this->getAuthority(); + $basePath = $this->getBasePath(); + $path = $this->getPath(); + $query = $this->getQuery(); + $fragment = $this->getFragment(); + + $path = $basePath . '/' . ltrim($path, '/'); + + return ($scheme ? $scheme . ':' : '') + . ($authority ? '//' . $authority : '') + . $path + . ($query ? '?' . $query : '') + . ($fragment ? '#' . $fragment : ''); + } + + /** + * Return the fully qualified base URL. + * + * Note that this method never includes a trailing / + * + * This method is not part of PSR-7. + * + * @return string + */ + public function getBaseUrl() + { + $scheme = $this->getScheme(); + $authority = $this->getAuthority(); + $basePath = $this->getBasePath(); + + if ($authority && substr($basePath, 0, 1) !== '/') { + $basePath = $basePath . '/' . $basePath; + } + + return ($scheme ? $scheme . ':' : '') + . ($authority ? '//' . $authority : '') + . rtrim($basePath, '/'); + } +} diff --git a/vendor/slim/slim/Slim/Interfaces/CallableResolverInterface.php b/vendor/slim/slim/Slim/Interfaces/CallableResolverInterface.php new file mode 100644 index 0000000..9bde59a --- /dev/null +++ b/vendor/slim/slim/Slim/Interfaces/CallableResolverInterface.php @@ -0,0 +1,27 @@ +middlewareLock) { + throw new RuntimeException('Middleware can’t be added once the stack is dequeuing'); + } + + if (is_null($this->stack)) { + $this->seedMiddlewareStack(); + } + $next = $this->stack->top(); + $this->stack[] = function (ServerRequestInterface $req, ResponseInterface $res) use ($callable, $next) { + $result = call_user_func($callable, $req, $res, $next); + if ($result instanceof ResponseInterface === false) { + throw new UnexpectedValueException( + 'Middleware must return instance of \Psr\Http\Message\ResponseInterface' + ); + } + + return $result; + }; + + return $this; + } + + /** + * Seed middleware stack with first callable + * + * @param callable $kernel The last item to run as middleware + * + * @throws RuntimeException if the stack is seeded more than once + */ + protected function seedMiddlewareStack(callable $kernel = null) + { + if (!is_null($this->stack)) { + throw new RuntimeException('MiddlewareStack can only be seeded once.'); + } + if ($kernel === null) { + $kernel = $this; + } + $this->stack = new SplStack; + $this->stack->setIteratorMode(SplDoublyLinkedList::IT_MODE_LIFO | SplDoublyLinkedList::IT_MODE_KEEP); + $this->stack[] = $kernel; + } + + /** + * Call middleware stack + * + * @param ServerRequestInterface $req A request object + * @param ResponseInterface $res A response object + * + * @return ResponseInterface + */ + public function callMiddlewareStack(ServerRequestInterface $req, ResponseInterface $res) + { + if (is_null($this->stack)) { + $this->seedMiddlewareStack(); + } + /** @var callable $start */ + $start = $this->stack->top(); + $this->middlewareLock = true; + $resp = $start($req, $res); + $this->middlewareLock = false; + return $resp; + } +} diff --git a/vendor/slim/slim/Slim/Routable.php b/vendor/slim/slim/Slim/Routable.php new file mode 100644 index 0000000..ac39868 --- /dev/null +++ b/vendor/slim/slim/Slim/Routable.php @@ -0,0 +1,96 @@ +middleware; + } + + /** + * Get the route pattern + * + * @return string + */ + public function getPattern() + { + return $this->pattern; + } + + /** + * Set container for use with resolveCallable + * + * @param ContainerInterface $container + * + * @return self + */ + public function setContainer(ContainerInterface $container) + { + $this->container = $container; + return $this; + } + + /** + * Prepend middleware to the middleware collection + * + * @param callable|string $callable The callback routine + * + * @return static + */ + public function add($callable) + { + $this->middleware[] = new DeferredCallable($callable, $this->container); + return $this; + } +} diff --git a/vendor/slim/slim/Slim/Route.php b/vendor/slim/slim/Slim/Route.php new file mode 100644 index 0000000..3cb4a0e --- /dev/null +++ b/vendor/slim/slim/Slim/Route.php @@ -0,0 +1,357 @@ +methods = $methods; + $this->pattern = $pattern; + $this->callable = $callable; + $this->groups = $groups; + $this->identifier = 'route' . $identifier; + } + + /** + * Finalize the route in preparation for dispatching + */ + public function finalize() + { + if ($this->finalized) { + return; + } + + $groupMiddleware = []; + foreach ($this->getGroups() as $group) { + $groupMiddleware = array_merge($group->getMiddleware(), $groupMiddleware); + } + + $this->middleware = array_merge($this->middleware, $groupMiddleware); + + foreach ($this->getMiddleware() as $middleware) { + $this->addMiddleware($middleware); + } + + $this->finalized = true; + } + + /** + * Get route callable + * + * @return callable + */ + public function getCallable() + { + return $this->callable; + } + + /** + * Get route methods + * + * @return string[] + */ + public function getMethods() + { + return $this->methods; + } + + /** + * Get parent route groups + * + * @return RouteGroup[] + */ + public function getGroups() + { + return $this->groups; + } + + /** + * Get route name + * + * @return null|string + */ + public function getName() + { + return $this->name; + } + + /** + * Get route identifier + * + * @return string + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * Get output buffering mode + * + * @return boolean|string + */ + public function getOutputBuffering() + { + return $this->outputBuffering; + } + + /** + * Set output buffering mode + * + * One of: false, 'prepend' or 'append' + * + * @param boolean|string $mode + * + * @throws InvalidArgumentException If an unknown buffering mode is specified + */ + public function setOutputBuffering($mode) + { + if (!in_array($mode, [false, 'prepend', 'append'], true)) { + throw new InvalidArgumentException('Unknown output buffering mode'); + } + $this->outputBuffering = $mode; + } + + /** + * Set route name + * + * @param string $name + * + * @return self + * + * @throws InvalidArgumentException if the route name is not a string + */ + public function setName($name) + { + if (!is_string($name)) { + throw new InvalidArgumentException('Route name must be a string'); + } + $this->name = $name; + return $this; + } + + /** + * Set a route argument + * + * @param string $name + * @param string $value + * + * @return self + */ + public function setArgument($name, $value) + { + $this->arguments[$name] = $value; + return $this; + } + + /** + * Replace route arguments + * + * @param array $arguments + * + * @return self + */ + public function setArguments(array $arguments) + { + $this->arguments = $arguments; + return $this; + } + + /** + * Retrieve route arguments + * + * @return array + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * Retrieve a specific route argument + * + * @param string $name + * @param mixed $default + * + * @return mixed + */ + public function getArgument($name, $default = null) + { + if (array_key_exists($name, $this->arguments)) { + return $this->arguments[$name]; + } + return $default; + } + + /******************************************************************************** + * Route Runner + *******************************************************************************/ + + /** + * Prepare the route for use + * + * @param ServerRequestInterface $request + * @param array $arguments + */ + public function prepare(ServerRequestInterface $request, array $arguments) + { + // Add the arguments + foreach ($arguments as $k => $v) { + $this->setArgument($k, $v); + } + } + + /** + * Run route + * + * This method traverses the middleware stack, including the route's callable + * and captures the resultant HTTP response object. It then sends the response + * back to the Application. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * + * @return ResponseInterface + */ + public function run(ServerRequestInterface $request, ResponseInterface $response) + { + // Finalise route now that we are about to run it + $this->finalize(); + + // Traverse middleware stack and fetch updated response + return $this->callMiddlewareStack($request, $response); + } + + /** + * Dispatch route callable against current Request and Response objects + * + * This method invokes the route object's callable. If middleware is + * registered for the route, each callable middleware is invoked in + * the order specified. + * + * @param ServerRequestInterface $request The current Request object + * @param ResponseInterface $response The current Response object + * @return \Psr\Http\Message\ResponseInterface + * @throws \Exception if the route callable throws an exception + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response) + { + $this->callable = $this->resolveCallable($this->callable); + + /** @var InvocationStrategyInterface $handler */ + $handler = isset($this->container) ? $this->container->get('foundHandler') : new RequestResponse(); + + // invoke route callable + if ($this->outputBuffering === false) { + $newResponse = $handler($this->callable, $request, $response, $this->arguments); + } else { + try { + ob_start(); + $newResponse = $handler($this->callable, $request, $response, $this->arguments); + $output = ob_get_clean(); + } catch (Exception $e) { + ob_end_clean(); + throw $e; + } + } + + if ($newResponse instanceof ResponseInterface) { + // if route callback returns a ResponseInterface, then use it + $response = $newResponse; + } elseif (is_string($newResponse)) { + // if route callback returns a string, then append it to the response + if ($response->getBody()->isWritable()) { + $response->getBody()->write($newResponse); + } + } + + if (!empty($output) && $response->getBody()->isWritable()) { + if ($this->outputBuffering === 'prepend') { + // prepend output buffer content + $body = new Http\Body(fopen('php://temp', 'r+')); + $body->write($output . $response->getBody()); + $response = $response->withBody($body); + } elseif ($this->outputBuffering === 'append') { + // append output buffer content + $response->getBody()->write($output); + } + } + + return $response; + } +} diff --git a/vendor/slim/slim/Slim/RouteGroup.php b/vendor/slim/slim/Slim/RouteGroup.php new file mode 100644 index 0000000..a0cdf4d --- /dev/null +++ b/vendor/slim/slim/Slim/RouteGroup.php @@ -0,0 +1,47 @@ +pattern = $pattern; + $this->callable = $callable; + } + + /** + * Invoke the group to register any Routable objects within it. + * + * @param App $app The App to bind the callable to. + */ + public function __invoke(App $app = null) + { + $callable = $this->resolveCallable($this->callable); + if ($callable instanceof Closure && $app !== null) { + $callable = $callable->bindTo($app); + } + + $callable(); + } +} diff --git a/vendor/slim/slim/Slim/Router.php b/vendor/slim/slim/Slim/Router.php new file mode 100644 index 0000000..1544e8d --- /dev/null +++ b/vendor/slim/slim/Slim/Router.php @@ -0,0 +1,361 @@ +routeParser = $parser ?: new StdParser; + } + + /** + * Set the base path used in pathFor() + * + * @param string $basePath + * + * @return self + */ + public function setBasePath($basePath) + { + if (!is_string($basePath)) { + throw new InvalidArgumentException('Router basePath must be a string'); + } + + $this->basePath = $basePath; + + return $this; + } + + /** + * Add route + * + * @param string[] $methods Array of HTTP methods + * @param string $pattern The route pattern + * @param callable $handler The route callable + * + * @return RouteInterface + * + * @throws InvalidArgumentException if the route pattern isn't a string + */ + public function map($methods, $pattern, $handler) + { + if (!is_string($pattern)) { + throw new InvalidArgumentException('Route pattern must be a string'); + } + + // Prepend parent group pattern(s) + if ($this->routeGroups) { + $pattern = $this->processGroups() . $pattern; + } + + // According to RFC methods are defined in uppercase (See RFC 7231) + $methods = array_map("strtoupper", $methods); + + // Add route + $route = new Route($methods, $pattern, $handler, $this->routeGroups, $this->routeCounter); + $this->routes[$route->getIdentifier()] = $route; + $this->routeCounter++; + + return $route; + } + + /** + * Dispatch router for HTTP request + * + * @param ServerRequestInterface $request The current HTTP request object + * + * @return array + * + * @link https://github.com/nikic/FastRoute/blob/master/src/Dispatcher.php + */ + public function dispatch(ServerRequestInterface $request) + { + $uri = '/' . ltrim($request->getUri()->getPath(), '/'); + + return $this->createDispatcher()->dispatch( + $request->getMethod(), + $uri + ); + } + + /** + * @return \FastRoute\Dispatcher + */ + protected function createDispatcher() + { + return $this->dispatcher ?: \FastRoute\simpleDispatcher(function (RouteCollector $r) { + foreach ($this->getRoutes() as $route) { + $r->addRoute($route->getMethods(), $route->getPattern(), $route->getIdentifier()); + } + }, [ + 'routeParser' => $this->routeParser + ]); + } + + /** + * @param \FastRoute\Dispatcher $dispatcher + */ + public function setDispatcher(Dispatcher $dispatcher) + { + $this->dispatcher = $dispatcher; + } + + /** + * Get route objects + * + * @return Route[] + */ + public function getRoutes() + { + return $this->routes; + } + + /** + * Get named route object + * + * @param string $name Route name + * + * @return Route + * + * @throws RuntimeException If named route does not exist + */ + public function getNamedRoute($name) + { + foreach ($this->routes as $route) { + if ($name == $route->getName()) { + return $route; + } + } + throw new RuntimeException('Named route does not exist for name: ' . $name); + } + + /** + * Process route groups + * + * @return string A group pattern to prefix routes with + */ + protected function processGroups() + { + $pattern = ""; + foreach ($this->routeGroups as $group) { + $pattern .= $group->getPattern(); + } + return $pattern; + } + + /** + * Add a route group to the array + * + * @param string $pattern + * @param callable $callable + * + * @return RouteGroupInterface + */ + public function pushGroup($pattern, $callable) + { + $group = new RouteGroup($pattern, $callable); + array_push($this->routeGroups, $group); + return $group; + } + + /** + * Removes the last route group from the array + * + * @return RouteGroup|bool The RouteGroup if successful, else False + */ + public function popGroup() + { + $group = array_pop($this->routeGroups); + return $group instanceof RouteGroup ? $group : false; + } + + /** + * @param $identifier + * @return \Slim\Interfaces\RouteInterface + */ + public function lookupRoute($identifier) + { + if (!isset($this->routes[$identifier])) { + throw new RuntimeException('Route not found, looks like your route cache is stale.'); + } + return $this->routes[$identifier]; + } + + /** + * Build the path for a named route excluding the base path + * + * @param string $name Route name + * @param array $data Named argument replacement data + * @param array $queryParams Optional query string parameters + * + * @return string + * + * @throws RuntimeException If named route does not exist + * @throws InvalidArgumentException If required data not provided + */ + public function relativePathFor($name, array $data = [], array $queryParams = []) + { + $route = $this->getNamedRoute($name); + $pattern = $route->getPattern(); + + $routeDatas = $this->routeParser->parse($pattern); + // $routeDatas is an array of all possible routes that can be made. There is + // one routedata for each optional parameter plus one for no optional parameters. + // + // The most specific is last, so we look for that first. + $routeDatas = array_reverse($routeDatas); + + $segments = []; + foreach ($routeDatas as $routeData) { + foreach ($routeData as $item) { + if (is_string($item)) { + // this segment is a static string + $segments[] = $item; + continue; + } + + // This segment has a parameter: first element is the name + if (!array_key_exists($item[0], $data)) { + // we don't have a data element for this segment: cancel + // testing this routeData item, so that we can try a less + // specific routeData item. + $segments = []; + $segmentName = $item[0]; + break; + } + $segments[] = $data[$item[0]]; + } + if (!empty($segments)) { + // we found all the parameters for this route data, no need to check + // less specific ones + break; + } + } + + if (empty($segments)) { + throw new InvalidArgumentException('Missing data for URL segment: ' . $segmentName); + } + $url = implode('', $segments); + + if ($queryParams) { + $url .= '?' . http_build_query($queryParams); + } + + return $url; + } + + + /** + * Build the path for a named route including the base path + * + * @param string $name Route name + * @param array $data Named argument replacement data + * @param array $queryParams Optional query string parameters + * + * @return string + * + * @throws RuntimeException If named route does not exist + * @throws InvalidArgumentException If required data not provided + */ + public function pathFor($name, array $data = [], array $queryParams = []) + { + $url = $this->relativePathFor($name, $data, $queryParams); + + if ($this->basePath) { + $url = $this->basePath . $url; + } + + return $url; + } + + /** + * Build the path for a named route. + * + * This method is deprecated. Use pathFor() from now on. + * + * @param string $name Route name + * @param array $data Named argument replacement data + * @param array $queryParams Optional query string parameters + * + * @return string + * + * @throws RuntimeException If named route does not exist + * @throws InvalidArgumentException If required data not provided + */ + public function urlFor($name, array $data = [], array $queryParams = []) + { + trigger_error('urlFor() is deprecated. Use pathFor() instead.', E_USER_DEPRECATED); + return $this->pathFor($name, $data, $queryParams); + } +} diff --git a/vendor/slim/slim/composer.json b/vendor/slim/slim/composer.json new file mode 100644 index 0000000..df18901 --- /dev/null +++ b/vendor/slim/slim/composer.json @@ -0,0 +1,54 @@ +{ + "name": "slim/slim", + "type": "library", + "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", + "keywords": ["framework","micro","api","router"], + "homepage": "http://slimframework.com", + "license": "MIT", + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "http://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "http://akrabat.com" + }, + { + "name": "Gabriel Manricks", + "email": "gmanricks@me.com", + "homepage": "http://gabrielmanricks.com" + } + ], + "require": { + "php": ">=5.5.0", + "pimple/pimple": "^3.0", + "psr/http-message": "^1.0", + "nikic/fast-route": "^0.6", + "container-interop/container-interop": "^1.1" + }, + "require-dev": { + "squizlabs/php_codesniffer": "^2.5", + "phpunit/phpunit": "^4.0" + }, + "autoload": { + "psr-4": { + "Slim\\": "Slim" + } + }, + "scripts": { + "test": [ + "php vendor/bin/phpcs", + "php vendor/bin/phpunit" + ], + "phpunit": "php vendor/bin/phpunit", + "phpcs": "php vendor/bin/phpcs" + } +} diff --git a/vendor/slim/slim/example/.htaccess b/vendor/slim/slim/example/.htaccess new file mode 100644 index 0000000..0784bd2 --- /dev/null +++ b/vendor/slim/slim/example/.htaccess @@ -0,0 +1,12 @@ +# Note: see https://httpd.apache.org/docs/current/howto/htaccess.html: +# +# "You should avoid using .htaccess files completely if you have access to +# httpd main server config file. Using .htaccess files slows down your Apache +# http server. Any directive that you can include in a .htaccess file is +# better set in a Directory block, as it will have the same effect with +# better performance." + +RewriteEngine On +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^ index.php [QSA,L] diff --git a/vendor/slim/slim/example/index.php b/vendor/slim/slim/example/index.php new file mode 100644 index 0000000..ef895f8 --- /dev/null +++ b/vendor/slim/slim/example/index.php @@ -0,0 +1,45 @@ +get('/', function ($request, $response, $args) { + $response->write("Welcome to Slim!"); + return $response; +}); + +$app->get('/hello[/{name}]', function ($request, $response, $args) { + $response->write("Hello, " . $args['name']); + return $response; +})->setArgument('name', 'World!'); + +/** + * Step 4: Run the Slim application + * + * This method should be called last. This executes the Slim application + * and returns the HTTP response to the HTTP client. + */ +$app->run(); diff --git a/vendor/symfony/yaml/.gitignore b/vendor/symfony/yaml/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/vendor/symfony/yaml/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/vendor/symfony/yaml/CHANGELOG.md b/vendor/symfony/yaml/CHANGELOG.md new file mode 100644 index 0000000..fe77de7 --- /dev/null +++ b/vendor/symfony/yaml/CHANGELOG.md @@ -0,0 +1,34 @@ +CHANGELOG +========= + +3.0.0 +----- + + * Yaml::parse() now throws an exception when a blackslash is not escaped + in double-quoted strings + +2.8.0 +----- + + * Deprecated usage of a colon in an unquoted mapping value + * Deprecated usage of @, \`, | and > at the beginning of an unquoted string + * When surrounding strings with double-quotes, you must now escape `\` characters. Not + escaping those characters (when surrounded by double-quotes) is deprecated. + + Before: + + ```yml + class: "Foo\Var" + ``` + + After: + + ```yml + class: "Foo\\Var" + ``` + +2.1.0 +----- + + * Yaml::parse() does not evaluate loaded files as PHP files by default + anymore (call Yaml::enablePhpParsing() to get back the old behavior) diff --git a/vendor/symfony/yaml/Dumper.php b/vendor/symfony/yaml/Dumper.php new file mode 100644 index 0000000..21351a5 --- /dev/null +++ b/vendor/symfony/yaml/Dumper.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml; + +/** + * Dumper dumps PHP variables to YAML strings. + * + * @author Fabien Potencier + */ +class Dumper +{ + /** + * The amount of spaces to use for indentation of nested nodes. + * + * @var int + */ + protected $indentation = 4; + + /** + * Sets the indentation. + * + * @param int $num The amount of spaces to use for indentation of nested nodes. + */ + public function setIndentation($num) + { + if ($num < 1) { + throw new \InvalidArgumentException('The indentation must be greater than zero.'); + } + + $this->indentation = (int) $num; + } + + /** + * Dumps a PHP value to YAML. + * + * @param mixed $input The PHP value + * @param int $inline The level where you switch to inline YAML + * @param int $indent The level of indentation (used internally) + * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise + * @param bool $objectSupport true if object support is enabled, false otherwise + * + * @return string The YAML representation of the PHP value + */ + public function dump($input, $inline = 0, $indent = 0, $exceptionOnInvalidType = false, $objectSupport = false) + { + $output = ''; + $prefix = $indent ? str_repeat(' ', $indent) : ''; + + if ($inline <= 0 || !is_array($input) || empty($input)) { + $output .= $prefix.Inline::dump($input, $exceptionOnInvalidType, $objectSupport); + } else { + $isAHash = array_keys($input) !== range(0, count($input) - 1); + + foreach ($input as $key => $value) { + $willBeInlined = $inline - 1 <= 0 || !is_array($value) || empty($value); + + $output .= sprintf('%s%s%s%s', + $prefix, + $isAHash ? Inline::dump($key, $exceptionOnInvalidType, $objectSupport).':' : '-', + $willBeInlined ? ' ' : "\n", + $this->dump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $exceptionOnInvalidType, $objectSupport) + ).($willBeInlined ? "\n" : ''); + } + } + + return $output; + } +} diff --git a/vendor/symfony/yaml/Escaper.php b/vendor/symfony/yaml/Escaper.php new file mode 100644 index 0000000..7bac8dd --- /dev/null +++ b/vendor/symfony/yaml/Escaper.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml; + +/** + * Escaper encapsulates escaping rules for single and double-quoted + * YAML strings. + * + * @author Matthew Lewinski + * + * @internal + */ +class Escaper +{ + // Characters that would cause a dumped string to require double quoting. + const REGEX_CHARACTER_TO_ESCAPE = "[\\x00-\\x1f]|\xc2\x85|\xc2\xa0|\xe2\x80\xa8|\xe2\x80\xa9"; + + // Mapping arrays for escaping a double quoted string. The backslash is + // first to ensure proper escaping because str_replace operates iteratively + // on the input arrays. This ordering of the characters avoids the use of strtr, + // which performs more slowly. + private static $escapees = array('\\', '\\\\', '\\"', '"', + "\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", + "\x08", "\x09", "\x0a", "\x0b", "\x0c", "\x0d", "\x0e", "\x0f", + "\x10", "\x11", "\x12", "\x13", "\x14", "\x15", "\x16", "\x17", + "\x18", "\x19", "\x1a", "\x1b", "\x1c", "\x1d", "\x1e", "\x1f", + "\xc2\x85", "\xc2\xa0", "\xe2\x80\xa8", "\xe2\x80\xa9"); + private static $escaped = array('\\\\', '\\"', '\\\\', '\\"', + '\\0', '\\x01', '\\x02', '\\x03', '\\x04', '\\x05', '\\x06', '\\a', + '\\b', '\\t', '\\n', '\\v', '\\f', '\\r', '\\x0e', '\\x0f', + '\\x10', '\\x11', '\\x12', '\\x13', '\\x14', '\\x15', '\\x16', '\\x17', + '\\x18', '\\x19', '\\x1a', '\\e', '\\x1c', '\\x1d', '\\x1e', '\\x1f', + '\\N', '\\_', '\\L', '\\P'); + + /** + * Determines if a PHP value would require double quoting in YAML. + * + * @param string $value A PHP value + * + * @return bool True if the value would require double quotes. + */ + public static function requiresDoubleQuoting($value) + { + return preg_match('/'.self::REGEX_CHARACTER_TO_ESCAPE.'/u', $value); + } + + /** + * Escapes and surrounds a PHP value with double quotes. + * + * @param string $value A PHP value + * + * @return string The quoted, escaped string + */ + public static function escapeWithDoubleQuotes($value) + { + return sprintf('"%s"', str_replace(self::$escapees, self::$escaped, $value)); + } + + /** + * Determines if a PHP value would require single quoting in YAML. + * + * @param string $value A PHP value + * + * @return bool True if the value would require single quotes. + */ + public static function requiresSingleQuoting($value) + { + // Determines if a PHP value is entirely composed of a value that would + // require single quoting in YAML. + if (in_array(strtolower($value), array('null', '~', 'true', 'false', 'y', 'n', 'yes', 'no', 'on', 'off'))) { + return true; + } + + // Determines if the PHP value contains any single characters that would + // cause it to require single quoting in YAML. + return preg_match('/[ \s \' " \: \{ \} \[ \] , & \* \# \?] | \A[ \- ? | < > = ! % @ ` ]/x', $value); + } + + /** + * Escapes and surrounds a PHP value with single quotes. + * + * @param string $value A PHP value + * + * @return string The quoted, escaped string + */ + public static function escapeWithSingleQuotes($value) + { + return sprintf("'%s'", str_replace('\'', '\'\'', $value)); + } +} diff --git a/vendor/symfony/yaml/Exception/DumpException.php b/vendor/symfony/yaml/Exception/DumpException.php new file mode 100644 index 0000000..cce972f --- /dev/null +++ b/vendor/symfony/yaml/Exception/DumpException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml\Exception; + +/** + * Exception class thrown when an error occurs during dumping. + * + * @author Fabien Potencier + */ +class DumpException extends RuntimeException +{ +} diff --git a/vendor/symfony/yaml/Exception/ExceptionInterface.php b/vendor/symfony/yaml/Exception/ExceptionInterface.php new file mode 100644 index 0000000..ad850ee --- /dev/null +++ b/vendor/symfony/yaml/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml\Exception; + +/** + * Exception interface for all exceptions thrown by the component. + * + * @author Fabien Potencier + */ +interface ExceptionInterface +{ +} diff --git a/vendor/symfony/yaml/Exception/ParseException.php b/vendor/symfony/yaml/Exception/ParseException.php new file mode 100644 index 0000000..ba3be7d --- /dev/null +++ b/vendor/symfony/yaml/Exception/ParseException.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml\Exception; + +/** + * Exception class thrown when an error occurs during parsing. + * + * @author Fabien Potencier + */ +class ParseException extends RuntimeException +{ + private $parsedFile; + private $parsedLine; + private $snippet; + private $rawMessage; + + /** + * Constructor. + * + * @param string $message The error message + * @param int $parsedLine The line where the error occurred + * @param int $snippet The snippet of code near the problem + * @param string $parsedFile The file name where the error occurred + * @param \Exception $previous The previous exception + */ + public function __construct($message, $parsedLine = -1, $snippet = null, $parsedFile = null, \Exception $previous = null) + { + $this->parsedFile = $parsedFile; + $this->parsedLine = $parsedLine; + $this->snippet = $snippet; + $this->rawMessage = $message; + + $this->updateRepr(); + + parent::__construct($this->message, 0, $previous); + } + + /** + * Gets the snippet of code near the error. + * + * @return string The snippet of code + */ + public function getSnippet() + { + return $this->snippet; + } + + /** + * Sets the snippet of code near the error. + * + * @param string $snippet The code snippet + */ + public function setSnippet($snippet) + { + $this->snippet = $snippet; + + $this->updateRepr(); + } + + /** + * Gets the filename where the error occurred. + * + * This method returns null if a string is parsed. + * + * @return string The filename + */ + public function getParsedFile() + { + return $this->parsedFile; + } + + /** + * Sets the filename where the error occurred. + * + * @param string $parsedFile The filename + */ + public function setParsedFile($parsedFile) + { + $this->parsedFile = $parsedFile; + + $this->updateRepr(); + } + + /** + * Gets the line where the error occurred. + * + * @return int The file line + */ + public function getParsedLine() + { + return $this->parsedLine; + } + + /** + * Sets the line where the error occurred. + * + * @param int $parsedLine The file line + */ + public function setParsedLine($parsedLine) + { + $this->parsedLine = $parsedLine; + + $this->updateRepr(); + } + + private function updateRepr() + { + $this->message = $this->rawMessage; + + $dot = false; + if ('.' === substr($this->message, -1)) { + $this->message = substr($this->message, 0, -1); + $dot = true; + } + + if (null !== $this->parsedFile) { + $this->message .= sprintf(' in %s', json_encode($this->parsedFile, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + } + + if ($this->parsedLine >= 0) { + $this->message .= sprintf(' at line %d', $this->parsedLine); + } + + if ($this->snippet) { + $this->message .= sprintf(' (near "%s")', $this->snippet); + } + + if ($dot) { + $this->message .= '.'; + } + } +} diff --git a/vendor/symfony/yaml/Exception/RuntimeException.php b/vendor/symfony/yaml/Exception/RuntimeException.php new file mode 100644 index 0000000..3f36b73 --- /dev/null +++ b/vendor/symfony/yaml/Exception/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml\Exception; + +/** + * Exception class thrown when an error occurs during parsing. + * + * @author Romain Neutron + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/yaml/Inline.php b/vendor/symfony/yaml/Inline.php new file mode 100644 index 0000000..10fbd89 --- /dev/null +++ b/vendor/symfony/yaml/Inline.php @@ -0,0 +1,568 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml; + +use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Exception\DumpException; + +/** + * Inline implements a YAML parser/dumper for the YAML inline syntax. + * + * @author Fabien Potencier + */ +class Inline +{ + const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\']*(?:\'\'[^\']*)*)\')'; + + private static $exceptionOnInvalidType = false; + private static $objectSupport = false; + private static $objectForMap = false; + + /** + * Converts a YAML string to a PHP array. + * + * @param string $value A YAML string + * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise + * @param bool $objectSupport true if object support is enabled, false otherwise + * @param bool $objectForMap true if maps should return a stdClass instead of array() + * @param array $references Mapping of variable names to values + * + * @return array A PHP array representing the YAML string + * + * @throws ParseException + */ + public static function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false, $references = array()) + { + self::$exceptionOnInvalidType = $exceptionOnInvalidType; + self::$objectSupport = $objectSupport; + self::$objectForMap = $objectForMap; + + $value = trim($value); + + if ('' === $value) { + return ''; + } + + if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) { + $mbEncoding = mb_internal_encoding(); + mb_internal_encoding('ASCII'); + } + + $i = 0; + switch ($value[0]) { + case '[': + $result = self::parseSequence($value, $i, $references); + ++$i; + break; + case '{': + $result = self::parseMapping($value, $i, $references); + ++$i; + break; + default: + $result = self::parseScalar($value, null, array('"', "'"), $i, true, $references); + } + + // some comments are allowed at the end + if (preg_replace('/\s+#.*$/A', '', substr($value, $i))) { + throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i))); + } + + if (isset($mbEncoding)) { + mb_internal_encoding($mbEncoding); + } + + return $result; + } + + /** + * Dumps a given PHP variable to a YAML string. + * + * @param mixed $value The PHP variable to convert + * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise + * @param bool $objectSupport true if object support is enabled, false otherwise + * + * @return string The YAML string representing the PHP array + * + * @throws DumpException When trying to dump PHP resource + */ + public static function dump($value, $exceptionOnInvalidType = false, $objectSupport = false) + { + switch (true) { + case is_resource($value): + if ($exceptionOnInvalidType) { + throw new DumpException(sprintf('Unable to dump PHP resources in a YAML file ("%s").', get_resource_type($value))); + } + + return 'null'; + case is_object($value): + if ($objectSupport) { + return '!php/object:'.serialize($value); + } + + if ($exceptionOnInvalidType) { + throw new DumpException('Object support when dumping a YAML file has been disabled.'); + } + + return 'null'; + case is_array($value): + return self::dumpArray($value, $exceptionOnInvalidType, $objectSupport); + case null === $value: + return 'null'; + case true === $value: + return 'true'; + case false === $value: + return 'false'; + case ctype_digit($value): + return is_string($value) ? "'$value'" : (int) $value; + case is_numeric($value): + $locale = setlocale(LC_NUMERIC, 0); + if (false !== $locale) { + setlocale(LC_NUMERIC, 'C'); + } + if (is_float($value)) { + $repr = (string) $value; + if (is_infinite($value)) { + $repr = str_ireplace('INF', '.Inf', $repr); + } elseif (floor($value) == $value && $repr == $value) { + // Preserve float data type since storing a whole number will result in integer value. + $repr = '!!float '.$repr; + } + } else { + $repr = is_string($value) ? "'$value'" : (string) $value; + } + if (false !== $locale) { + setlocale(LC_NUMERIC, $locale); + } + + return $repr; + case '' == $value: + return "''"; + case Escaper::requiresDoubleQuoting($value): + return Escaper::escapeWithDoubleQuotes($value); + case Escaper::requiresSingleQuoting($value): + case preg_match(self::getHexRegex(), $value): + case preg_match(self::getTimestampRegex(), $value): + return Escaper::escapeWithSingleQuotes($value); + default: + return $value; + } + } + + /** + * Dumps a PHP array to a YAML string. + * + * @param array $value The PHP array to dump + * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise + * @param bool $objectSupport true if object support is enabled, false otherwise + * + * @return string The YAML string representing the PHP array + */ + private static function dumpArray($value, $exceptionOnInvalidType, $objectSupport) + { + // array + $keys = array_keys($value); + $keysCount = count($keys); + if ((1 === $keysCount && '0' == $keys[0]) + || ($keysCount > 1 && array_reduce($keys, function ($v, $w) { return (int) $v + $w; }, 0) === $keysCount * ($keysCount - 1) / 2) + ) { + $output = array(); + foreach ($value as $val) { + $output[] = self::dump($val, $exceptionOnInvalidType, $objectSupport); + } + + return sprintf('[%s]', implode(', ', $output)); + } + + // mapping + $output = array(); + foreach ($value as $key => $val) { + $output[] = sprintf('%s: %s', self::dump($key, $exceptionOnInvalidType, $objectSupport), self::dump($val, $exceptionOnInvalidType, $objectSupport)); + } + + return sprintf('{ %s }', implode(', ', $output)); + } + + /** + * Parses a scalar to a YAML string. + * + * @param string $scalar + * @param string $delimiters + * @param array $stringDelimiters + * @param int &$i + * @param bool $evaluate + * @param array $references + * + * @return string A YAML string + * + * @throws ParseException When malformed inline YAML string is parsed + * + * @internal + */ + public static function parseScalar($scalar, $delimiters = null, $stringDelimiters = array('"', "'"), &$i = 0, $evaluate = true, $references = array()) + { + if (in_array($scalar[$i], $stringDelimiters)) { + // quoted scalar + $output = self::parseQuotedScalar($scalar, $i); + + if (null !== $delimiters) { + $tmp = ltrim(substr($scalar, $i), ' '); + if (!in_array($tmp[0], $delimiters)) { + throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i))); + } + } + } else { + // "normal" string + if (!$delimiters) { + $output = substr($scalar, $i); + $i += strlen($output); + + // remove comments + if (preg_match('/[ \t]+#/', $output, $match, PREG_OFFSET_CAPTURE)) { + $output = substr($output, 0, $match[0][1]); + } + } elseif (preg_match('/^(.+?)('.implode('|', $delimiters).')/', substr($scalar, $i), $match)) { + $output = $match[1]; + $i += strlen($output); + } else { + throw new ParseException(sprintf('Malformed inline YAML string (%s).', $scalar)); + } + + // a non-quoted string cannot start with @ or ` (reserved) nor with a scalar indicator (| or >) + if ($output && ('@' === $output[0] || '`' === $output[0] || '|' === $output[0] || '>' === $output[0])) { + throw new ParseException(sprintf('The reserved indicator "%s" cannot start a plain scalar; you need to quote the scalar.', $output[0])); + } + + if ($evaluate) { + $output = self::evaluateScalar($output, $references); + } + } + + return $output; + } + + /** + * Parses a quoted scalar to YAML. + * + * @param string $scalar + * @param int &$i + * + * @return string A YAML string + * + * @throws ParseException When malformed inline YAML string is parsed + */ + private static function parseQuotedScalar($scalar, &$i) + { + if (!preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) { + throw new ParseException(sprintf('Malformed inline YAML string (%s).', substr($scalar, $i))); + } + + $output = substr($match[0], 1, strlen($match[0]) - 2); + + $unescaper = new Unescaper(); + if ('"' == $scalar[$i]) { + $output = $unescaper->unescapeDoubleQuotedString($output); + } else { + $output = $unescaper->unescapeSingleQuotedString($output); + } + + $i += strlen($match[0]); + + return $output; + } + + /** + * Parses a sequence to a YAML string. + * + * @param string $sequence + * @param int &$i + * @param array $references + * + * @return string A YAML string + * + * @throws ParseException When malformed inline YAML string is parsed + */ + private static function parseSequence($sequence, &$i = 0, $references = array()) + { + $output = array(); + $len = strlen($sequence); + ++$i; + + // [foo, bar, ...] + while ($i < $len) { + switch ($sequence[$i]) { + case '[': + // nested sequence + $output[] = self::parseSequence($sequence, $i, $references); + break; + case '{': + // nested mapping + $output[] = self::parseMapping($sequence, $i, $references); + break; + case ']': + return $output; + case ',': + case ' ': + break; + default: + $isQuoted = in_array($sequence[$i], array('"', "'")); + $value = self::parseScalar($sequence, array(',', ']'), array('"', "'"), $i, true, $references); + + // the value can be an array if a reference has been resolved to an array var + if (!is_array($value) && !$isQuoted && false !== strpos($value, ': ')) { + // embedded mapping? + try { + $pos = 0; + $value = self::parseMapping('{'.$value.'}', $pos, $references); + } catch (\InvalidArgumentException $e) { + // no, it's not + } + } + + $output[] = $value; + + --$i; + } + + ++$i; + } + + throw new ParseException(sprintf('Malformed inline YAML string %s', $sequence)); + } + + /** + * Parses a mapping to a YAML string. + * + * @param string $mapping + * @param int &$i + * @param array $references + * + * @return string A YAML string + * + * @throws ParseException When malformed inline YAML string is parsed + */ + private static function parseMapping($mapping, &$i = 0, $references = array()) + { + $output = array(); + $len = strlen($mapping); + ++$i; + + // {foo: bar, bar:foo, ...} + while ($i < $len) { + switch ($mapping[$i]) { + case ' ': + case ',': + ++$i; + continue 2; + case '}': + if (self::$objectForMap) { + return (object) $output; + } + + return $output; + } + + // key + $key = self::parseScalar($mapping, array(':', ' '), array('"', "'"), $i, false); + + // value + $done = false; + + while ($i < $len) { + switch ($mapping[$i]) { + case '[': + // nested sequence + $value = self::parseSequence($mapping, $i, $references); + // Spec: Keys MUST be unique; first one wins. + // Parser cannot abort this mapping earlier, since lines + // are processed sequentially. + if (!isset($output[$key])) { + $output[$key] = $value; + } + $done = true; + break; + case '{': + // nested mapping + $value = self::parseMapping($mapping, $i, $references); + // Spec: Keys MUST be unique; first one wins. + // Parser cannot abort this mapping earlier, since lines + // are processed sequentially. + if (!isset($output[$key])) { + $output[$key] = $value; + } + $done = true; + break; + case ':': + case ' ': + break; + default: + $value = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i, true, $references); + // Spec: Keys MUST be unique; first one wins. + // Parser cannot abort this mapping earlier, since lines + // are processed sequentially. + if (!isset($output[$key])) { + $output[$key] = $value; + } + $done = true; + --$i; + } + + ++$i; + + if ($done) { + continue 2; + } + } + } + + throw new ParseException(sprintf('Malformed inline YAML string %s', $mapping)); + } + + /** + * Evaluates scalars and replaces magic values. + * + * @param string $scalar + * @param array $references + * + * @return string A YAML string + * + * @throws ParseException when object parsing support was disabled and the parser detected a PHP object or when a reference could not be resolved + */ + private static function evaluateScalar($scalar, $references = array()) + { + $scalar = trim($scalar); + $scalarLower = strtolower($scalar); + + if (0 === strpos($scalar, '*')) { + if (false !== $pos = strpos($scalar, '#')) { + $value = substr($scalar, 1, $pos - 2); + } else { + $value = substr($scalar, 1); + } + + // an unquoted * + if (false === $value || '' === $value) { + throw new ParseException('A reference must contain at least one character.'); + } + + if (!array_key_exists($value, $references)) { + throw new ParseException(sprintf('Reference "%s" does not exist.', $value)); + } + + return $references[$value]; + } + + switch (true) { + case 'null' === $scalarLower: + case '' === $scalar: + case '~' === $scalar: + return; + case 'true' === $scalarLower: + return true; + case 'false' === $scalarLower: + return false; + // Optimise for returning strings. + case $scalar[0] === '+' || $scalar[0] === '-' || $scalar[0] === '.' || $scalar[0] === '!' || is_numeric($scalar[0]): + switch (true) { + case 0 === strpos($scalar, '!str'): + return (string) substr($scalar, 5); + case 0 === strpos($scalar, '! '): + return (int) self::parseScalar(substr($scalar, 2)); + case 0 === strpos($scalar, '!php/object:'): + if (self::$objectSupport) { + return unserialize(substr($scalar, 12)); + } + + if (self::$exceptionOnInvalidType) { + throw new ParseException('Object support when parsing a YAML file has been disabled.'); + } + + return; + case 0 === strpos($scalar, '!!php/object:'): + if (self::$objectSupport) { + return unserialize(substr($scalar, 13)); + } + + if (self::$exceptionOnInvalidType) { + throw new ParseException('Object support when parsing a YAML file has been disabled.'); + } + + return; + case 0 === strpos($scalar, '!!float '): + return (float) substr($scalar, 8); + case ctype_digit($scalar): + $raw = $scalar; + $cast = (int) $scalar; + + return '0' == $scalar[0] ? octdec($scalar) : (((string) $raw == (string) $cast) ? $cast : $raw); + case '-' === $scalar[0] && ctype_digit(substr($scalar, 1)): + $raw = $scalar; + $cast = (int) $scalar; + + return '0' == $scalar[1] ? octdec($scalar) : (((string) $raw === (string) $cast) ? $cast : $raw); + case is_numeric($scalar): + case preg_match(self::getHexRegex(), $scalar): + return '0x' === $scalar[0].$scalar[1] ? hexdec($scalar) : (float) $scalar; + case '.inf' === $scalarLower: + case '.nan' === $scalarLower: + return -log(0); + case '-.inf' === $scalarLower: + return log(0); + case preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $scalar): + return (float) str_replace(',', '', $scalar); + case preg_match(self::getTimestampRegex(), $scalar): + $timeZone = date_default_timezone_get(); + date_default_timezone_set('UTC'); + $time = strtotime($scalar); + date_default_timezone_set($timeZone); + + return $time; + } + default: + return (string) $scalar; + } + } + + /** + * Gets a regex that matches a YAML date. + * + * @return string The regular expression + * + * @see http://www.yaml.org/spec/1.2/spec.html#id2761573 + */ + private static function getTimestampRegex() + { + return <<[0-9][0-9][0-9][0-9]) + -(?P[0-9][0-9]?) + -(?P[0-9][0-9]?) + (?:(?:[Tt]|[ \t]+) + (?P[0-9][0-9]?) + :(?P[0-9][0-9]) + :(?P[0-9][0-9]) + (?:\.(?P[0-9]*))? + (?:[ \t]*(?PZ|(?P[-+])(?P[0-9][0-9]?) + (?::(?P[0-9][0-9]))?))?)? + $~x +EOF; + } + + /** + * Gets a regex that matches a YAML number in hexadecimal notation. + * + * @return string + */ + private static function getHexRegex() + { + return '~^0x[0-9a-f]++$~i'; + } +} diff --git a/vendor/symfony/yaml/LICENSE b/vendor/symfony/yaml/LICENSE new file mode 100644 index 0000000..12a7453 --- /dev/null +++ b/vendor/symfony/yaml/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2016 Fabien Potencier + +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: + +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/vendor/symfony/yaml/Parser.php b/vendor/symfony/yaml/Parser.php new file mode 100644 index 0000000..28b58c3 --- /dev/null +++ b/vendor/symfony/yaml/Parser.php @@ -0,0 +1,778 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml; + +use Symfony\Component\Yaml\Exception\ParseException; + +/** + * Parser parses YAML strings to convert them to PHP arrays. + * + * @author Fabien Potencier + */ +class Parser +{ + const BLOCK_SCALAR_HEADER_PATTERN = '(?P\||>)(?P\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P +#.*)?'; + + private $offset = 0; + private $lines = array(); + private $currentLineNb = -1; + private $currentLine = ''; + private $refs = array(); + + /** + * Constructor. + * + * @param int $offset The offset of YAML document (used for line numbers in error messages) + */ + public function __construct($offset = 0) + { + $this->offset = $offset; + } + + /** + * Parses a YAML string to a PHP value. + * + * @param string $value A YAML string + * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise + * @param bool $objectSupport true if object support is enabled, false otherwise + * @param bool $objectForMap true if maps should return a stdClass instead of array() + * + * @return mixed A PHP value + * + * @throws ParseException If the YAML is not valid + */ + public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false) + { + if (!preg_match('//u', $value)) { + throw new ParseException('The YAML value does not appear to be valid UTF-8.'); + } + $this->currentLineNb = -1; + $this->currentLine = ''; + $value = $this->cleanup($value); + $this->lines = explode("\n", $value); + + if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) { + $mbEncoding = mb_internal_encoding(); + mb_internal_encoding('UTF-8'); + } + + $data = array(); + $context = null; + $allowOverwrite = false; + while ($this->moveToNextLine()) { + if ($this->isCurrentLineEmpty()) { + continue; + } + + // tab? + if ("\t" === $this->currentLine[0]) { + throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine); + } + + $isRef = $mergeNode = false; + if (preg_match('#^\-((?P\s+)(?P.+?))?\s*$#u', $this->currentLine, $values)) { + if ($context && 'mapping' == $context) { + throw new ParseException('You cannot define a sequence item when in a mapping'); + } + $context = 'sequence'; + + if (isset($values['value']) && preg_match('#^&(?P[^ ]+) *(?P.*)#u', $values['value'], $matches)) { + $isRef = $matches['ref']; + $values['value'] = $matches['value']; + } + + // array + if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) { + $c = $this->getRealCurrentLineNb() + 1; + $parser = new self($c); + $parser->refs = &$this->refs; + $data[] = $parser->parse($this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap); + } else { + if (isset($values['leadspaces']) + && preg_match('#^(?P'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P.+?))?\s*$#u', $values['value'], $matches) + ) { + // this is a compact notation element, add to next block and parse + $c = $this->getRealCurrentLineNb(); + $parser = new self($c); + $parser->refs = &$this->refs; + + $block = $values['value']; + if ($this->isNextLineIndented()) { + $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1); + } + + $data[] = $parser->parse($block, $exceptionOnInvalidType, $objectSupport, $objectForMap); + } else { + $data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context); + } + } + if ($isRef) { + $this->refs[$isRef] = end($data); + } + } elseif (preg_match('#^(?P'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P.+?))?\s*$#u', $this->currentLine, $values) && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))) { + if ($context && 'sequence' == $context) { + throw new ParseException('You cannot define a mapping item when in a sequence'); + } + $context = 'mapping'; + + // force correct settings + Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs); + try { + $key = Inline::parseScalar($values['key']); + } catch (ParseException $e) { + $e->setParsedLine($this->getRealCurrentLineNb() + 1); + $e->setSnippet($this->currentLine); + + throw $e; + } + + // Convert float keys to strings, to avoid being converted to integers by PHP + if (is_float($key)) { + $key = (string) $key; + } + + if ('<<' === $key) { + $mergeNode = true; + $allowOverwrite = true; + if (isset($values['value']) && 0 === strpos($values['value'], '*')) { + $refName = substr($values['value'], 1); + if (!array_key_exists($refName, $this->refs)) { + throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine); + } + + $refValue = $this->refs[$refName]; + + if (!is_array($refValue)) { + throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine); + } + + foreach ($refValue as $key => $value) { + if (!isset($data[$key])) { + $data[$key] = $value; + } + } + } else { + if (isset($values['value']) && $values['value'] !== '') { + $value = $values['value']; + } else { + $value = $this->getNextEmbedBlock(); + } + $c = $this->getRealCurrentLineNb() + 1; + $parser = new self($c); + $parser->refs = &$this->refs; + $parsed = $parser->parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap); + + if (!is_array($parsed)) { + throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine); + } + + if (isset($parsed[0])) { + // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes + // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier + // in the sequence override keys specified in later mapping nodes. + foreach ($parsed as $parsedItem) { + if (!is_array($parsedItem)) { + throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem); + } + + foreach ($parsedItem as $key => $value) { + if (!isset($data[$key])) { + $data[$key] = $value; + } + } + } + } else { + // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the + // current mapping, unless the key already exists in it. + foreach ($parsed as $key => $value) { + if (!isset($data[$key])) { + $data[$key] = $value; + } + } + } + } + } elseif (isset($values['value']) && preg_match('#^&(?P[^ ]+) *(?P.*)#u', $values['value'], $matches)) { + $isRef = $matches['ref']; + $values['value'] = $matches['value']; + } + + if ($mergeNode) { + // Merge keys + } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) { + // hash + // if next line is less indented or equal, then it means that the current value is null + if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) { + // Spec: Keys MUST be unique; first one wins. + // But overwriting is allowed when a merge node is used in current block. + if ($allowOverwrite || !isset($data[$key])) { + $data[$key] = null; + } + } else { + $c = $this->getRealCurrentLineNb() + 1; + $parser = new self($c); + $parser->refs = &$this->refs; + $value = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap); + // Spec: Keys MUST be unique; first one wins. + // But overwriting is allowed when a merge node is used in current block. + if ($allowOverwrite || !isset($data[$key])) { + $data[$key] = $value; + } + } + } else { + $value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context); + // Spec: Keys MUST be unique; first one wins. + // But overwriting is allowed when a merge node is used in current block. + if ($allowOverwrite || !isset($data[$key])) { + $data[$key] = $value; + } + } + if ($isRef) { + $this->refs[$isRef] = $data[$key]; + } + } else { + // multiple documents are not supported + if ('---' === $this->currentLine) { + throw new ParseException('Multiple documents are not supported.'); + } + + // 1-liner optionally followed by newline(s) + if (is_string($value) && $this->lines[0] === trim($value)) { + try { + $value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs); + } catch (ParseException $e) { + $e->setParsedLine($this->getRealCurrentLineNb() + 1); + $e->setSnippet($this->currentLine); + + throw $e; + } + + if (is_array($value)) { + $first = reset($value); + if (is_string($first) && 0 === strpos($first, '*')) { + $data = array(); + foreach ($value as $alias) { + $data[] = $this->refs[substr($alias, 1)]; + } + $value = $data; + } + } + + if (isset($mbEncoding)) { + mb_internal_encoding($mbEncoding); + } + + return $value; + } + + switch (preg_last_error()) { + case PREG_INTERNAL_ERROR: + $error = 'Internal PCRE error.'; + break; + case PREG_BACKTRACK_LIMIT_ERROR: + $error = 'pcre.backtrack_limit reached.'; + break; + case PREG_RECURSION_LIMIT_ERROR: + $error = 'pcre.recursion_limit reached.'; + break; + case PREG_BAD_UTF8_ERROR: + $error = 'Malformed UTF-8 data.'; + break; + case PREG_BAD_UTF8_OFFSET_ERROR: + $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.'; + break; + default: + $error = 'Unable to parse.'; + } + + throw new ParseException($error, $this->getRealCurrentLineNb() + 1, $this->currentLine); + } + } + + if (isset($mbEncoding)) { + mb_internal_encoding($mbEncoding); + } + + if ($objectForMap && !is_object($data) && 'mapping' === $context) { + $object = new \stdClass(); + + foreach ($data as $key => $value) { + $object->$key = $value; + } + + $data = $object; + } + + return empty($data) ? null : $data; + } + + /** + * Returns the current line number (takes the offset into account). + * + * @return int The current line number + */ + private function getRealCurrentLineNb() + { + return $this->currentLineNb + $this->offset; + } + + /** + * Returns the current line indentation. + * + * @return int The current line indentation + */ + private function getCurrentLineIndentation() + { + return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' ')); + } + + /** + * Returns the next embed block of YAML. + * + * @param int $indentation The indent level at which the block is to be read, or null for default + * @param bool $inSequence True if the enclosing data structure is a sequence + * + * @return string A YAML string + * + * @throws ParseException When indentation problem are detected + */ + private function getNextEmbedBlock($indentation = null, $inSequence = false) + { + $oldLineIndentation = $this->getCurrentLineIndentation(); + $blockScalarIndentations = array(); + + if ($this->isBlockScalarHeader()) { + $blockScalarIndentations[] = $this->getCurrentLineIndentation(); + } + + if (!$this->moveToNextLine()) { + return; + } + + if (null === $indentation) { + $newIndent = $this->getCurrentLineIndentation(); + + $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem(); + + if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) { + throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine); + } + } else { + $newIndent = $indentation; + } + + $data = array(); + if ($this->getCurrentLineIndentation() >= $newIndent) { + $data[] = substr($this->currentLine, $newIndent); + } else { + $this->moveToPreviousLine(); + + return; + } + + if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) { + // the previous line contained a dash but no item content, this line is a sequence item with the same indentation + // and therefore no nested list or mapping + $this->moveToPreviousLine(); + + return; + } + + $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem(); + + if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) { + $blockScalarIndentations[] = $this->getCurrentLineIndentation(); + } + + $previousLineIndentation = $this->getCurrentLineIndentation(); + + while ($this->moveToNextLine()) { + $indent = $this->getCurrentLineIndentation(); + + // terminate all block scalars that are more indented than the current line + if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && trim($this->currentLine) !== '') { + foreach ($blockScalarIndentations as $key => $blockScalarIndentation) { + if ($blockScalarIndentation >= $this->getCurrentLineIndentation()) { + unset($blockScalarIndentations[$key]); + } + } + } + + if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) { + $blockScalarIndentations[] = $this->getCurrentLineIndentation(); + } + + $previousLineIndentation = $indent; + + if ($isItUnindentedCollection && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) { + $this->moveToPreviousLine(); + break; + } + + if ($this->isCurrentLineBlank()) { + $data[] = substr($this->currentLine, $newIndent); + continue; + } + + // we ignore "comment" lines only when we are not inside a scalar block + if (empty($blockScalarIndentations) && $this->isCurrentLineComment()) { + continue; + } + + if ($indent >= $newIndent) { + $data[] = substr($this->currentLine, $newIndent); + } elseif (0 == $indent) { + $this->moveToPreviousLine(); + + break; + } else { + throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine); + } + } + + return implode("\n", $data); + } + + /** + * Moves the parser to the next line. + * + * @return bool + */ + private function moveToNextLine() + { + if ($this->currentLineNb >= count($this->lines) - 1) { + return false; + } + + $this->currentLine = $this->lines[++$this->currentLineNb]; + + return true; + } + + /** + * Moves the parser to the previous line. + */ + private function moveToPreviousLine() + { + $this->currentLine = $this->lines[--$this->currentLineNb]; + } + + /** + * Parses a YAML value. + * + * @param string $value A YAML value + * @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise + * @param bool $objectSupport True if object support is enabled, false otherwise + * @param bool $objectForMap true if maps should return a stdClass instead of array() + * @param string $context The parser context (either sequence or mapping) + * + * @return mixed A PHP value + * + * @throws ParseException When reference does not exist + */ + private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $context) + { + if (0 === strpos($value, '*')) { + if (false !== $pos = strpos($value, '#')) { + $value = substr($value, 1, $pos - 2); + } else { + $value = substr($value, 1); + } + + if (!array_key_exists($value, $this->refs)) { + throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLine); + } + + return $this->refs[$value]; + } + + if (preg_match('/^'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) { + $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : ''; + + return $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers)); + } + + try { + $parsedValue = Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs); + + if ('mapping' === $context && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) { + throw new ParseException('A colon cannot be used in an unquoted mapping value.'); + } + + return $parsedValue; + } catch (ParseException $e) { + $e->setParsedLine($this->getRealCurrentLineNb() + 1); + $e->setSnippet($this->currentLine); + + throw $e; + } + } + + /** + * Parses a block scalar. + * + * @param string $style The style indicator that was used to begin this block scalar (| or >) + * @param string $chomping The chomping indicator that was used to begin this block scalar (+ or -) + * @param int $indentation The indentation indicator that was used to begin this block scalar + * + * @return string The text value + */ + private function parseBlockScalar($style, $chomping = '', $indentation = 0) + { + $notEOF = $this->moveToNextLine(); + if (!$notEOF) { + return ''; + } + + $isCurrentLineBlank = $this->isCurrentLineBlank(); + $blockLines = array(); + + // leading blank lines are consumed before determining indentation + while ($notEOF && $isCurrentLineBlank) { + // newline only if not EOF + if ($notEOF = $this->moveToNextLine()) { + $blockLines[] = ''; + $isCurrentLineBlank = $this->isCurrentLineBlank(); + } + } + + // determine indentation if not specified + if (0 === $indentation) { + if (preg_match('/^ +/', $this->currentLine, $matches)) { + $indentation = strlen($matches[0]); + } + } + + if ($indentation > 0) { + $pattern = sprintf('/^ {%d}(.*)$/', $indentation); + + while ( + $notEOF && ( + $isCurrentLineBlank || + preg_match($pattern, $this->currentLine, $matches) + ) + ) { + if ($isCurrentLineBlank && strlen($this->currentLine) > $indentation) { + $blockLines[] = substr($this->currentLine, $indentation); + } elseif ($isCurrentLineBlank) { + $blockLines[] = ''; + } else { + $blockLines[] = $matches[1]; + } + + // newline only if not EOF + if ($notEOF = $this->moveToNextLine()) { + $isCurrentLineBlank = $this->isCurrentLineBlank(); + } + } + } elseif ($notEOF) { + $blockLines[] = ''; + } + + if ($notEOF) { + $blockLines[] = ''; + $this->moveToPreviousLine(); + } + + // folded style + if ('>' === $style) { + $text = ''; + $previousLineIndented = false; + $previousLineBlank = false; + + for ($i = 0; $i < count($blockLines); ++$i) { + if ('' === $blockLines[$i]) { + $text .= "\n"; + $previousLineIndented = false; + $previousLineBlank = true; + } elseif (' ' === $blockLines[$i][0]) { + $text .= "\n".$blockLines[$i]; + $previousLineIndented = true; + $previousLineBlank = false; + } elseif ($previousLineIndented) { + $text .= "\n".$blockLines[$i]; + $previousLineIndented = false; + $previousLineBlank = false; + } elseif ($previousLineBlank || 0 === $i) { + $text .= $blockLines[$i]; + $previousLineIndented = false; + $previousLineBlank = false; + } else { + $text .= ' '.$blockLines[$i]; + $previousLineIndented = false; + $previousLineBlank = false; + } + } + } else { + $text = implode("\n", $blockLines); + } + + // deal with trailing newlines + if ('' === $chomping) { + $text = preg_replace('/\n+$/', "\n", $text); + } elseif ('-' === $chomping) { + $text = preg_replace('/\n+$/', '', $text); + } + + return $text; + } + + /** + * Returns true if the next line is indented. + * + * @return bool Returns true if the next line is indented, false otherwise + */ + private function isNextLineIndented() + { + $currentIndentation = $this->getCurrentLineIndentation(); + $EOF = !$this->moveToNextLine(); + + while (!$EOF && $this->isCurrentLineEmpty()) { + $EOF = !$this->moveToNextLine(); + } + + if ($EOF) { + return false; + } + + $ret = false; + if ($this->getCurrentLineIndentation() > $currentIndentation) { + $ret = true; + } + + $this->moveToPreviousLine(); + + return $ret; + } + + /** + * Returns true if the current line is blank or if it is a comment line. + * + * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise + */ + private function isCurrentLineEmpty() + { + return $this->isCurrentLineBlank() || $this->isCurrentLineComment(); + } + + /** + * Returns true if the current line is blank. + * + * @return bool Returns true if the current line is blank, false otherwise + */ + private function isCurrentLineBlank() + { + return '' == trim($this->currentLine, ' '); + } + + /** + * Returns true if the current line is a comment line. + * + * @return bool Returns true if the current line is a comment line, false otherwise + */ + private function isCurrentLineComment() + { + //checking explicitly the first char of the trim is faster than loops or strpos + $ltrimmedLine = ltrim($this->currentLine, ' '); + + return '' !== $ltrimmedLine && $ltrimmedLine[0] === '#'; + } + + /** + * Cleanups a YAML string to be parsed. + * + * @param string $value The input YAML string + * + * @return string A cleaned up YAML string + */ + private function cleanup($value) + { + $value = str_replace(array("\r\n", "\r"), "\n", $value); + + // strip YAML header + $count = 0; + $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count); + $this->offset += $count; + + // remove leading comments + $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count); + if ($count == 1) { + // items have been removed, update the offset + $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); + $value = $trimmedValue; + } + + // remove start of the document marker (---) + $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count); + if ($count == 1) { + // items have been removed, update the offset + $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); + $value = $trimmedValue; + + // remove end of the document marker (...) + $value = preg_replace('#\.\.\.\s*$#', '', $value); + } + + return $value; + } + + /** + * Returns true if the next line starts unindented collection. + * + * @return bool Returns true if the next line starts unindented collection, false otherwise + */ + private function isNextLineUnIndentedCollection() + { + $currentIndentation = $this->getCurrentLineIndentation(); + $notEOF = $this->moveToNextLine(); + + while ($notEOF && $this->isCurrentLineEmpty()) { + $notEOF = $this->moveToNextLine(); + } + + if (false === $notEOF) { + return false; + } + + $ret = false; + if ( + $this->getCurrentLineIndentation() == $currentIndentation + && + $this->isStringUnIndentedCollectionItem() + ) { + $ret = true; + } + + $this->moveToPreviousLine(); + + return $ret; + } + + /** + * Returns true if the string is un-indented collection item. + * + * @return bool Returns true if the string is un-indented collection item, false otherwise + */ + private function isStringUnIndentedCollectionItem() + { + return 0 === strpos($this->currentLine, '- '); + } + + /** + * Tests whether or not the current line is the header of a block scalar. + * + * @return bool + */ + private function isBlockScalarHeader() + { + return (bool) preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine); + } +} diff --git a/vendor/symfony/yaml/README.md b/vendor/symfony/yaml/README.md new file mode 100644 index 0000000..0d32488 --- /dev/null +++ b/vendor/symfony/yaml/README.md @@ -0,0 +1,13 @@ +Yaml Component +============== + +The Yaml component loads and dumps YAML files. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/yaml/index.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/vendor/symfony/yaml/Tests/DumperTest.php b/vendor/symfony/yaml/Tests/DumperTest.php new file mode 100644 index 0000000..84069d8 --- /dev/null +++ b/vendor/symfony/yaml/Tests/DumperTest.php @@ -0,0 +1,254 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml\Tests; + +use Symfony\Component\Yaml\Parser; +use Symfony\Component\Yaml\Dumper; + +class DumperTest extends \PHPUnit_Framework_TestCase +{ + protected $parser; + protected $dumper; + protected $path; + + protected $array = array( + '' => 'bar', + 'foo' => '#bar', + 'foo\'bar' => array(), + 'bar' => array(1, 'foo'), + 'foobar' => array( + 'foo' => 'bar', + 'bar' => array(1, 'foo'), + 'foobar' => array( + 'foo' => 'bar', + 'bar' => array(1, 'foo'), + ), + ), + ); + + protected function setUp() + { + $this->parser = new Parser(); + $this->dumper = new Dumper(); + $this->path = __DIR__.'/Fixtures'; + } + + protected function tearDown() + { + $this->parser = null; + $this->dumper = null; + $this->path = null; + $this->array = null; + } + + public function testSetIndentation() + { + $this->dumper->setIndentation(7); + + $expected = <<<'EOF' +'': bar +foo: '#bar' +'foo''bar': { } +bar: + - 1 + - foo +foobar: + foo: bar + bar: + - 1 + - foo + foobar: + foo: bar + bar: + - 1 + - foo + +EOF; + $this->assertEquals($expected, $this->dumper->dump($this->array, 4, 0)); + } + + public function testSpecifications() + { + $files = $this->parser->parse(file_get_contents($this->path.'/index.yml')); + foreach ($files as $file) { + $yamls = file_get_contents($this->path.'/'.$file.'.yml'); + + // split YAMLs documents + foreach (preg_split('/^---( %YAML\:1\.0)?/m', $yamls) as $yaml) { + if (!$yaml) { + continue; + } + + $test = $this->parser->parse($yaml); + if (isset($test['dump_skip']) && $test['dump_skip']) { + continue; + } elseif (isset($test['todo']) && $test['todo']) { + // TODO + } else { + eval('$expected = '.trim($test['php']).';'); + $this->assertSame($expected, $this->parser->parse($this->dumper->dump($expected, 10)), $test['test']); + } + } + } + } + + public function testInlineLevel() + { + $expected = <<<'EOF' +{ '': bar, foo: '#bar', 'foo''bar': { }, bar: [1, foo], foobar: { foo: bar, bar: [1, foo], foobar: { foo: bar, bar: [1, foo] } } } +EOF; + $this->assertEquals($expected, $this->dumper->dump($this->array, -10), '->dump() takes an inline level argument'); + $this->assertEquals($expected, $this->dumper->dump($this->array, 0), '->dump() takes an inline level argument'); + + $expected = <<<'EOF' +'': bar +foo: '#bar' +'foo''bar': { } +bar: [1, foo] +foobar: { foo: bar, bar: [1, foo], foobar: { foo: bar, bar: [1, foo] } } + +EOF; + $this->assertEquals($expected, $this->dumper->dump($this->array, 1), '->dump() takes an inline level argument'); + + $expected = <<<'EOF' +'': bar +foo: '#bar' +'foo''bar': { } +bar: + - 1 + - foo +foobar: + foo: bar + bar: [1, foo] + foobar: { foo: bar, bar: [1, foo] } + +EOF; + $this->assertEquals($expected, $this->dumper->dump($this->array, 2), '->dump() takes an inline level argument'); + + $expected = <<<'EOF' +'': bar +foo: '#bar' +'foo''bar': { } +bar: + - 1 + - foo +foobar: + foo: bar + bar: + - 1 + - foo + foobar: + foo: bar + bar: [1, foo] + +EOF; + $this->assertEquals($expected, $this->dumper->dump($this->array, 3), '->dump() takes an inline level argument'); + + $expected = <<<'EOF' +'': bar +foo: '#bar' +'foo''bar': { } +bar: + - 1 + - foo +foobar: + foo: bar + bar: + - 1 + - foo + foobar: + foo: bar + bar: + - 1 + - foo + +EOF; + $this->assertEquals($expected, $this->dumper->dump($this->array, 4), '->dump() takes an inline level argument'); + $this->assertEquals($expected, $this->dumper->dump($this->array, 10), '->dump() takes an inline level argument'); + } + + public function testObjectSupportEnabled() + { + $dump = $this->dumper->dump(array('foo' => new A(), 'bar' => 1), 0, 0, false, true); + + $this->assertEquals('{ foo: !php/object:O:30:"Symfony\Component\Yaml\Tests\A":1:{s:1:"a";s:3:"foo";}, bar: 1 }', $dump, '->dump() is able to dump objects'); + } + + public function testObjectSupportDisabledButNoExceptions() + { + $dump = $this->dumper->dump(array('foo' => new A(), 'bar' => 1)); + + $this->assertEquals('{ foo: null, bar: 1 }', $dump, '->dump() does not dump objects when disabled'); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\DumpException + */ + public function testObjectSupportDisabledWithExceptions() + { + $this->dumper->dump(array('foo' => new A(), 'bar' => 1), 0, 0, true, false); + } + + /** + * @dataProvider getEscapeSequences + */ + public function testEscapedEscapeSequencesInQuotedScalar($input, $expected) + { + $this->assertEquals($expected, $this->dumper->dump($input)); + } + + public function getEscapeSequences() + { + return array( + 'null' => array("\t\\0", '"\t\\\\0"'), + 'bell' => array("\t\\a", '"\t\\\\a"'), + 'backspace' => array("\t\\b", '"\t\\\\b"'), + 'horizontal-tab' => array("\t\\t", '"\t\\\\t"'), + 'line-feed' => array("\t\\n", '"\t\\\\n"'), + 'vertical-tab' => array("\t\\v", '"\t\\\\v"'), + 'form-feed' => array("\t\\f", '"\t\\\\f"'), + 'carriage-return' => array("\t\\r", '"\t\\\\r"'), + 'escape' => array("\t\\e", '"\t\\\\e"'), + 'space' => array("\t\\ ", '"\t\\\\ "'), + 'double-quote' => array("\t\\\"", '"\t\\\\\\""'), + 'slash' => array("\t\\/", '"\t\\\\/"'), + 'backslash' => array("\t\\\\", '"\t\\\\\\\\"'), + 'next-line' => array("\t\\N", '"\t\\\\N"'), + 'non-breaking-space' => array("\t\\�", '"\t\\\\�"'), + 'line-separator' => array("\t\\L", '"\t\\\\L"'), + 'paragraph-separator' => array("\t\\P", '"\t\\\\P"'), + ); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The indentation must be greater than zero + */ + public function testZeroIndentationThrowsException() + { + $this->dumper->setIndentation(0); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The indentation must be greater than zero + */ + public function testNegativeIndentationThrowsException() + { + $this->dumper->setIndentation(-4); + } +} + +class A +{ + public $a = 'foo'; +} diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsAnchorAlias.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsAnchorAlias.yml new file mode 100644 index 0000000..5f9c942 --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/YtsAnchorAlias.yml @@ -0,0 +1,31 @@ +--- %YAML:1.0 +test: Simple Alias Example +brief: > + If you need to refer to the same item of data twice, + you can give that item an alias. The alias is a plain + string, starting with an ampersand. The item may then + be referred to by the alias throughout your document + by using an asterisk before the name of the alias. + This is called an anchor. +yaml: | + - &showell Steve + - Clark + - Brian + - Oren + - *showell +php: | + array('Steve', 'Clark', 'Brian', 'Oren', 'Steve') + +--- +test: Alias of a Mapping +brief: > + An alias can be used on any item of data, including + sequences, mappings, and other complex data types. +yaml: | + - &hello + Meat: pork + Starch: potato + - banana + - *hello +php: | + array(array('Meat'=>'pork', 'Starch'=>'potato'), 'banana', array('Meat'=>'pork', 'Starch'=>'potato')) diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsBasicTests.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsBasicTests.yml new file mode 100644 index 0000000..dfd9302 --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/YtsBasicTests.yml @@ -0,0 +1,202 @@ +--- %YAML:1.0 +test: Simple Sequence +brief: | + You can specify a list in YAML by placing each + member of the list on a new line with an opening + dash. These lists are called sequences. +yaml: | + - apple + - banana + - carrot +php: | + array('apple', 'banana', 'carrot') +--- +test: Sequence With Item Being Null In The Middle +brief: | + You can specify a list in YAML by placing each + member of the list on a new line with an opening + dash. These lists are called sequences. +yaml: | + - apple + - + - carrot +php: | + array('apple', null, 'carrot') +--- +test: Sequence With Last Item Being Null +brief: | + You can specify a list in YAML by placing each + member of the list on a new line with an opening + dash. These lists are called sequences. +yaml: | + - apple + - banana + - +php: | + array('apple', 'banana', null) +--- +test: Nested Sequences +brief: | + You can include a sequence within another + sequence by giving the sequence an empty + dash, followed by an indented list. +yaml: | + - + - foo + - bar + - baz +php: | + array(array('foo', 'bar', 'baz')) +--- +test: Mixed Sequences +brief: | + Sequences can contain any YAML data, + including strings and other sequences. +yaml: | + - apple + - + - foo + - bar + - x123 + - banana + - carrot +php: | + array('apple', array('foo', 'bar', 'x123'), 'banana', 'carrot') +--- +test: Deeply Nested Sequences +brief: | + Sequences can be nested even deeper, with each + level of indentation representing a level of + depth. +yaml: | + - + - + - uno + - dos +php: | + array(array(array('uno', 'dos'))) +--- +test: Simple Mapping +brief: | + You can add a keyed list (also known as a dictionary or + hash) to your document by placing each member of the + list on a new line, with a colon separating the key + from its value. In YAML, this type of list is called + a mapping. +yaml: | + foo: whatever + bar: stuff +php: | + array('foo' => 'whatever', 'bar' => 'stuff') +--- +test: Sequence in a Mapping +brief: | + A value in a mapping can be a sequence. +yaml: | + foo: whatever + bar: + - uno + - dos +php: | + array('foo' => 'whatever', 'bar' => array('uno', 'dos')) +--- +test: Nested Mappings +brief: | + A value in a mapping can be another mapping. +yaml: | + foo: whatever + bar: + fruit: apple + name: steve + sport: baseball +php: | + array( + 'foo' => 'whatever', + 'bar' => array( + 'fruit' => 'apple', + 'name' => 'steve', + 'sport' => 'baseball' + ) + ) +--- +test: Mixed Mapping +brief: | + A mapping can contain any assortment + of mappings and sequences as values. +yaml: | + foo: whatever + bar: + - + fruit: apple + name: steve + sport: baseball + - more + - + python: rocks + perl: papers + ruby: scissorses +php: | + array( + 'foo' => 'whatever', + 'bar' => array( + array( + 'fruit' => 'apple', + 'name' => 'steve', + 'sport' => 'baseball' + ), + 'more', + array( + 'python' => 'rocks', + 'perl' => 'papers', + 'ruby' => 'scissorses' + ) + ) + ) +--- +test: Mapping-in-Sequence Shortcut +todo: true +brief: | + If you are adding a mapping to a sequence, you + can place the mapping on the same line as the + dash as a shortcut. +yaml: | + - work on YAML.py: + - work on Store +php: | + array(array('work on YAML.py' => array('work on Store'))) +--- +test: Sequence-in-Mapping Shortcut +todo: true +brief: | + The dash in a sequence counts as indentation, so + you can add a sequence inside of a mapping without + needing spaces as indentation. +yaml: | + allow: + - 'localhost' + - '%.sourceforge.net' + - '%.freepan.org' +php: | + array('allow' => array('localhost', '%.sourceforge.net', '%.freepan.org')) +--- +todo: true +test: Merge key +brief: | + A merge key ('<<') can be used in a mapping to insert other mappings. If + the value associated with the merge key is a mapping, each of its key/value + pairs is inserted into the current mapping. +yaml: | + mapping: + name: Joe + job: Accountant + <<: + age: 38 +php: | + array( + 'mapping' => + array( + 'name' => 'Joe', + 'job' => 'Accountant', + 'age' => 38 + ) + ) diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsBlockMapping.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsBlockMapping.yml new file mode 100644 index 0000000..f7ca469 --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/YtsBlockMapping.yml @@ -0,0 +1,51 @@ +--- +test: One Element Mapping +brief: | + A mapping with one key/value pair +yaml: | + foo: bar +php: | + array('foo' => 'bar') +--- +test: Multi Element Mapping +brief: | + More than one key/value pair +yaml: | + red: baron + white: walls + blue: berries +php: | + array( + 'red' => 'baron', + 'white' => 'walls', + 'blue' => 'berries', + ) +--- +test: Values aligned +brief: | + Often times human editors of documents will align the values even + though YAML emitters generally don't. +yaml: | + red: baron + white: walls + blue: berries +php: | + array( + 'red' => 'baron', + 'white' => 'walls', + 'blue' => 'berries', + ) +--- +test: Colons aligned +brief: | + Spaces can come before the ': ' key/value separator. +yaml: | + red : baron + white : walls + blue : berries +php: | + array( + 'red' => 'baron', + 'white' => 'walls', + 'blue' => 'berries', + ) diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsDocumentSeparator.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsDocumentSeparator.yml new file mode 100644 index 0000000..d988102 --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/YtsDocumentSeparator.yml @@ -0,0 +1,85 @@ +--- %YAML:1.0 +test: Trailing Document Separator +todo: true +brief: > + You can separate YAML documents + with a string of three dashes. +yaml: | + - foo: 1 + bar: 2 + --- + more: stuff +python: | + [ + [ { 'foo': 1, 'bar': 2 } ], + { 'more': 'stuff' } + ] +ruby: | + [ { 'foo' => 1, 'bar' => 2 } ] + +--- +test: Leading Document Separator +todo: true +brief: > + You can explicitly give an opening + document separator to your YAML stream. +yaml: | + --- + - foo: 1 + bar: 2 + --- + more: stuff +python: | + [ + [ {'foo': 1, 'bar': 2}], + {'more': 'stuff'} + ] +ruby: | + [ { 'foo' => 1, 'bar' => 2 } ] + +--- +test: YAML Header +todo: true +brief: > + The opening separator can contain directives + to the YAML parser, such as the version + number. +yaml: | + --- %YAML:1.0 + foo: 1 + bar: 2 +php: | + array('foo' => 1, 'bar' => 2) +documents: 1 + +--- +test: Red Herring Document Separator +brief: > + Separators included in blocks or strings + are treated as blocks or strings, as the + document separator should have no indentation + preceding it. +yaml: | + foo: | + --- +php: | + array('foo' => "---\n") + +--- +test: Multiple Document Separators in Block +brief: > + This technique allows you to embed other YAML + documents within literal blocks. +yaml: | + foo: | + --- + foo: bar + --- + yo: baz + bar: | + fooness +php: | + array( + 'foo' => "---\nfoo: bar\n---\nyo: baz\n", + 'bar' => "fooness\n" + ) diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsErrorTests.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsErrorTests.yml new file mode 100644 index 0000000..e8506fc --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/YtsErrorTests.yml @@ -0,0 +1,25 @@ +--- +test: Missing value for hash item +todo: true +brief: | + Third item in this hash doesn't have a value +yaml: | + okay: value + also okay: ~ + causes error because no value specified + last key: value okay here too +python-error: causes error because no value specified + +--- +test: Not indenting enough +brief: | + There was a bug in PyYaml where it was off by one + in the indentation check. It was allowing the YAML + below. +# This is actually valid YAML now. Someone should tell showell. +yaml: | + foo: + firstline: 1 + secondline: 2 +php: | + array('foo' => null, 'firstline' => 1, 'secondline' => 2) diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsFlowCollections.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsFlowCollections.yml new file mode 100644 index 0000000..03090e4 --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/YtsFlowCollections.yml @@ -0,0 +1,60 @@ +--- +test: Simple Inline Array +brief: > + Sequences can be contained on a + single line, using the inline syntax. + Separate each entry with commas and + enclose in square brackets. +yaml: | + seq: [ a, b, c ] +php: | + array('seq' => array('a', 'b', 'c')) +--- +test: Simple Inline Hash +brief: > + Mapping can also be contained on + a single line, using the inline + syntax. Each key-value pair is + separated by a colon, with a comma + between each entry in the mapping. + Enclose with curly braces. +yaml: | + hash: { name: Steve, foo: bar } +php: | + array('hash' => array('name' => 'Steve', 'foo' => 'bar')) +--- +test: Multi-line Inline Collections +todo: true +brief: > + Both inline sequences and inline mappings + can span multiple lines, provided that you + indent the additional lines. +yaml: | + languages: [ Ruby, + Perl, + Python ] + websites: { YAML: yaml.org, + Ruby: ruby-lang.org, + Python: python.org, + Perl: use.perl.org } +php: | + array( + 'languages' => array('Ruby', 'Perl', 'Python'), + 'websites' => array( + 'YAML' => 'yaml.org', + 'Ruby' => 'ruby-lang.org', + 'Python' => 'python.org', + 'Perl' => 'use.perl.org' + ) + ) +--- +test: Commas in Values (not in the spec!) +todo: true +brief: > + List items in collections are delimited by commas, but + there must be a space after each comma. This allows you + to add numbers without quoting. +yaml: | + attendances: [ 45,123, 70,000, 17,222 ] +php: | + array('attendances' => array(45123, 70000, 17222)) diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsFoldedScalars.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsFoldedScalars.yml new file mode 100644 index 0000000..a14735a --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/YtsFoldedScalars.yml @@ -0,0 +1,176 @@ +--- %YAML:1.0 +test: Single ending newline +brief: > + A pipe character, followed by an indented + block of text is treated as a literal + block, in which newlines are preserved + throughout the block, including the final + newline. +yaml: | + --- + this: | + Foo + Bar +php: | + array('this' => "Foo\nBar\n") +--- +test: The '+' indicator +brief: > + The '+' indicator says to keep newlines at the end of text + blocks. +yaml: | + normal: | + extra new lines not kept + + preserving: |+ + extra new lines are kept + + + dummy: value +php: | + array( + 'normal' => "extra new lines not kept\n", + 'preserving' => "extra new lines are kept\n\n\n", + 'dummy' => 'value' + ) +--- +test: Three trailing newlines in literals +brief: > + To give you more control over how space + is preserved in text blocks, YAML has + the keep '+' and chomp '-' indicators. + The keep indicator will preserve all + ending newlines, while the chomp indicator + will strip all ending newlines. +yaml: | + clipped: | + This has one newline. + + + + same as "clipped" above: "This has one newline.\n" + + stripped: |- + This has no newline. + + + + same as "stripped" above: "This has no newline." + + kept: |+ + This has four newlines. + + + + same as "kept" above: "This has four newlines.\n\n\n\n" +php: | + array( + 'clipped' => "This has one newline.\n", + 'same as "clipped" above' => "This has one newline.\n", + 'stripped' => 'This has no newline.', + 'same as "stripped" above' => 'This has no newline.', + 'kept' => "This has four newlines.\n\n\n\n", + 'same as "kept" above' => "This has four newlines.\n\n\n\n" + ) +--- +test: Extra trailing newlines with spaces +todo: true +brief: > + Normally, only a single newline is kept + from the end of a literal block, unless the + keep '+' character is used in combination + with the pipe. The following example + will preserve all ending whitespace + since the last line of both literal blocks + contains spaces which extend past the indentation + level. +yaml: | + --- + this: | + Foo + + + kept: |+ + Foo + + +php: | + array('this' => "Foo\n\n \n", + 'kept' => "Foo\n\n \n" ) + +--- +test: Folded Block in a Sequence +brief: > + A greater-then character, followed by an indented + block of text is treated as a folded block, in + which lines of text separated by a single newline + are concatenated as a single line. +yaml: | + --- + - apple + - banana + - > + can't you see + the beauty of yaml? + hmm + - dog +php: | + array( + 'apple', + 'banana', + "can't you see the beauty of yaml? hmm\n", + 'dog' + ) +--- +test: Folded Block as a Mapping Value +brief: > + Both literal and folded blocks can be + used in collections, as values in a + sequence or a mapping. +yaml: | + --- + quote: > + Mark McGwire's + year was crippled + by a knee injury. + source: espn +php: | + array( + 'quote' => "Mark McGwire's year was crippled by a knee injury.\n", + 'source' => 'espn' + ) +--- +test: Three trailing newlines in folded blocks +brief: > + The keep and chomp indicators can also + be applied to folded blocks. +yaml: | + clipped: > + This has one newline. + + + + same as "clipped" above: "This has one newline.\n" + + stripped: >- + This has no newline. + + + + same as "stripped" above: "This has no newline." + + kept: >+ + This has four newlines. + + + + same as "kept" above: "This has four newlines.\n\n\n\n" +php: | + array( + 'clipped' => "This has one newline.\n", + 'same as "clipped" above' => "This has one newline.\n", + 'stripped' => 'This has no newline.', + 'same as "stripped" above' => 'This has no newline.', + 'kept' => "This has four newlines.\n\n\n\n", + 'same as "kept" above' => "This has four newlines.\n\n\n\n" + ) diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsNullsAndEmpties.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsNullsAndEmpties.yml new file mode 100644 index 0000000..9a5300f --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/YtsNullsAndEmpties.yml @@ -0,0 +1,45 @@ +--- %YAML:1.0 +test: Empty Sequence +brief: > + You can represent the empty sequence + with an empty inline sequence. +yaml: | + empty: [] +php: | + array('empty' => array()) +--- +test: Empty Mapping +brief: > + You can represent the empty mapping + with an empty inline mapping. +yaml: | + empty: {} +php: | + array('empty' => array()) +--- +test: Empty Sequence as Entire Document +yaml: | + [] +php: | + array() +--- +test: Empty Mapping as Entire Document +yaml: | + {} +php: | + array() +--- +test: Null as Document +yaml: | + ~ +php: | + null +--- +test: Empty String +brief: > + You can represent an empty string + with a pair of quotes. +yaml: | + '' +php: | + '' diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsSpecificationExamples.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsSpecificationExamples.yml new file mode 100644 index 0000000..ec1c4c3 --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/YtsSpecificationExamples.yml @@ -0,0 +1,1697 @@ +--- %YAML:1.0 +test: Sequence of scalars +spec: 2.1 +yaml: | + - Mark McGwire + - Sammy Sosa + - Ken Griffey +php: | + array('Mark McGwire', 'Sammy Sosa', 'Ken Griffey') +--- +test: Mapping of scalars to scalars +spec: 2.2 +yaml: | + hr: 65 + avg: 0.278 + rbi: 147 +php: | + array('hr' => 65, 'avg' => 0.278, 'rbi' => 147) +--- +test: Mapping of scalars to sequences +spec: 2.3 +yaml: | + american: + - Boston Red Sox + - Detroit Tigers + - New York Yankees + national: + - New York Mets + - Chicago Cubs + - Atlanta Braves +php: | + array('american' => + array( 'Boston Red Sox', 'Detroit Tigers', + 'New York Yankees' ), + 'national' => + array( 'New York Mets', 'Chicago Cubs', + 'Atlanta Braves' ) + ) +--- +test: Sequence of mappings +spec: 2.4 +yaml: | + - + name: Mark McGwire + hr: 65 + avg: 0.278 + - + name: Sammy Sosa + hr: 63 + avg: 0.288 +php: | + array( + array('name' => 'Mark McGwire', 'hr' => 65, 'avg' => 0.278), + array('name' => 'Sammy Sosa', 'hr' => 63, 'avg' => 0.288) + ) +--- +test: Legacy A5 +todo: true +spec: legacy_A5 +yaml: | + ? + - New York Yankees + - Atlanta Braves + : + - 2001-07-02 + - 2001-08-12 + - 2001-08-14 + ? + - Detroit Tigers + - Chicago Cubs + : + - 2001-07-23 +perl-busted: > + YAML.pm will be able to emulate this behavior soon. In this regard + it may be somewhat more correct than Python's native behaviour which + can only use tuples as mapping keys. PyYAML will also need to figure + out some clever way to roundtrip structured keys. +python: | + [ + { + ('New York Yankees', 'Atlanta Braves'): + [yaml.timestamp('2001-07-02'), + yaml.timestamp('2001-08-12'), + yaml.timestamp('2001-08-14')], + ('Detroit Tigers', 'Chicago Cubs'): + [yaml.timestamp('2001-07-23')] + } + ] +ruby: | + { + [ 'New York Yankees', 'Atlanta Braves' ] => + [ Date.new( 2001, 7, 2 ), Date.new( 2001, 8, 12 ), Date.new( 2001, 8, 14 ) ], + [ 'Detroit Tigers', 'Chicago Cubs' ] => + [ Date.new( 2001, 7, 23 ) ] + } +syck: | + struct test_node seq1[] = { + { T_STR, 0, "New York Yankees" }, + { T_STR, 0, "Atlanta Braves" }, + end_node + }; + struct test_node seq2[] = { + { T_STR, 0, "2001-07-02" }, + { T_STR, 0, "2001-08-12" }, + { T_STR, 0, "2001-08-14" }, + end_node + }; + struct test_node seq3[] = { + { T_STR, 0, "Detroit Tigers" }, + { T_STR, 0, "Chicago Cubs" }, + end_node + }; + struct test_node seq4[] = { + { T_STR, 0, "2001-07-23" }, + end_node + }; + struct test_node map[] = { + { T_SEQ, 0, 0, seq1 }, + { T_SEQ, 0, 0, seq2 }, + { T_SEQ, 0, 0, seq3 }, + { T_SEQ, 0, 0, seq4 }, + end_node + }; + struct test_node stream[] = { + { T_MAP, 0, 0, map }, + end_node + }; + +--- +test: Sequence of sequences +spec: 2.5 +yaml: | + - [ name , hr , avg ] + - [ Mark McGwire , 65 , 0.278 ] + - [ Sammy Sosa , 63 , 0.288 ] +php: | + array( + array( 'name', 'hr', 'avg' ), + array( 'Mark McGwire', 65, 0.278 ), + array( 'Sammy Sosa', 63, 0.288 ) + ) +--- +test: Mapping of mappings +todo: true +spec: 2.6 +yaml: | + Mark McGwire: {hr: 65, avg: 0.278} + Sammy Sosa: { + hr: 63, + avg: 0.288 + } +php: | + array( + 'Mark McGwire' => + array( 'hr' => 65, 'avg' => 0.278 ), + 'Sammy Sosa' => + array( 'hr' => 63, 'avg' => 0.288 ) + ) +--- +test: Two documents in a stream each with a leading comment +todo: true +spec: 2.7 +yaml: | + # Ranking of 1998 home runs + --- + - Mark McGwire + - Sammy Sosa + - Ken Griffey + + # Team ranking + --- + - Chicago Cubs + - St Louis Cardinals +ruby: | + y = YAML::Stream.new + y.add( [ 'Mark McGwire', 'Sammy Sosa', 'Ken Griffey' ] ) + y.add( [ 'Chicago Cubs', 'St Louis Cardinals' ] ) +documents: 2 + +--- +test: Play by play feed from a game +todo: true +spec: 2.8 +yaml: | + --- + time: 20:03:20 + player: Sammy Sosa + action: strike (miss) + ... + --- + time: 20:03:47 + player: Sammy Sosa + action: grand slam + ... +perl: | + [ 'Mark McGwire', 'Sammy Sosa', 'Ken Griffey' ] +documents: 2 + +--- +test: Single document with two comments +spec: 2.9 +yaml: | + hr: # 1998 hr ranking + - Mark McGwire + - Sammy Sosa + rbi: + # 1998 rbi ranking + - Sammy Sosa + - Ken Griffey +php: | + array( + 'hr' => array( 'Mark McGwire', 'Sammy Sosa' ), + 'rbi' => array( 'Sammy Sosa', 'Ken Griffey' ) + ) +--- +test: Node for Sammy Sosa appears twice in this document +spec: 2.10 +yaml: | + --- + hr: + - Mark McGwire + # Following node labeled SS + - &SS Sammy Sosa + rbi: + - *SS # Subsequent occurrence + - Ken Griffey +php: | + array( + 'hr' => + array('Mark McGwire', 'Sammy Sosa'), + 'rbi' => + array('Sammy Sosa', 'Ken Griffey') + ) +--- +test: Mapping between sequences +todo: true +spec: 2.11 +yaml: | + ? # PLAY SCHEDULE + - Detroit Tigers + - Chicago Cubs + : + - 2001-07-23 + + ? [ New York Yankees, + Atlanta Braves ] + : [ 2001-07-02, 2001-08-12, + 2001-08-14 ] +ruby: | + { + [ 'Detroit Tigers', 'Chicago Cubs' ] => [ Date.new( 2001, 7, 23 ) ], + [ 'New York Yankees', 'Atlanta Braves' ] => [ Date.new( 2001, 7, 2 ), Date.new( 2001, 8, 12 ), Date.new( 2001, 8, 14 ) ] + } +syck: | + struct test_node seq1[] = { + { T_STR, 0, "New York Yankees" }, + { T_STR, 0, "Atlanta Braves" }, + end_node + }; + struct test_node seq2[] = { + { T_STR, 0, "2001-07-02" }, + { T_STR, 0, "2001-08-12" }, + { T_STR, 0, "2001-08-14" }, + end_node + }; + struct test_node seq3[] = { + { T_STR, 0, "Detroit Tigers" }, + { T_STR, 0, "Chicago Cubs" }, + end_node + }; + struct test_node seq4[] = { + { T_STR, 0, "2001-07-23" }, + end_node + }; + struct test_node map[] = { + { T_SEQ, 0, 0, seq3 }, + { T_SEQ, 0, 0, seq4 }, + { T_SEQ, 0, 0, seq1 }, + { T_SEQ, 0, 0, seq2 }, + end_node + }; + struct test_node stream[] = { + { T_MAP, 0, 0, map }, + end_node + }; + +--- +test: Sequence key shortcut +spec: 2.12 +yaml: | + --- + # products purchased + - item : Super Hoop + quantity: 1 + - item : Basketball + quantity: 4 + - item : Big Shoes + quantity: 1 +php: | + array ( + array ( + 'item' => 'Super Hoop', + 'quantity' => 1, + ), + array ( + 'item' => 'Basketball', + 'quantity' => 4, + ), + array ( + 'item' => 'Big Shoes', + 'quantity' => 1, + ) + ) +perl: | + [ + { item => 'Super Hoop', quantity => 1 }, + { item => 'Basketball', quantity => 4 }, + { item => 'Big Shoes', quantity => 1 } + ] + +ruby: | + [ + { 'item' => 'Super Hoop', 'quantity' => 1 }, + { 'item' => 'Basketball', 'quantity' => 4 }, + { 'item' => 'Big Shoes', 'quantity' => 1 } + ] +python: | + [ + { 'item': 'Super Hoop', 'quantity': 1 }, + { 'item': 'Basketball', 'quantity': 4 }, + { 'item': 'Big Shoes', 'quantity': 1 } + ] +syck: | + struct test_node map1[] = { + { T_STR, 0, "item" }, + { T_STR, 0, "Super Hoop" }, + { T_STR, 0, "quantity" }, + { T_STR, 0, "1" }, + end_node + }; + struct test_node map2[] = { + { T_STR, 0, "item" }, + { T_STR, 0, "Basketball" }, + { T_STR, 0, "quantity" }, + { T_STR, 0, "4" }, + end_node + }; + struct test_node map3[] = { + { T_STR, 0, "item" }, + { T_STR, 0, "Big Shoes" }, + { T_STR, 0, "quantity" }, + { T_STR, 0, "1" }, + end_node + }; + struct test_node seq[] = { + { T_MAP, 0, 0, map1 }, + { T_MAP, 0, 0, map2 }, + { T_MAP, 0, 0, map3 }, + end_node + }; + struct test_node stream[] = { + { T_SEQ, 0, 0, seq }, + end_node + }; + + +--- +test: Literal perserves newlines +todo: true +spec: 2.13 +yaml: | + # ASCII Art + --- | + \//||\/|| + // || ||_ +perl: | + "\\//||\\/||\n// || ||_\n" +ruby: | + "\\//||\\/||\n// || ||_\n" +python: | + [ + flushLeft( + """ + \//||\/|| + // || ||_ + """ + ) + ] +syck: | + struct test_node stream[] = { + { T_STR, 0, "\\//||\\/||\n// || ||_\n" }, + end_node + }; + +--- +test: Folded treats newlines as a space +todo: true +spec: 2.14 +yaml: | + --- + Mark McGwire's + year was crippled + by a knee injury. +perl: | + "Mark McGwire's year was crippled by a knee injury." +ruby: | + "Mark McGwire's year was crippled by a knee injury." +python: | + [ "Mark McGwire's year was crippled by a knee injury." ] +syck: | + struct test_node stream[] = { + { T_STR, 0, "Mark McGwire's year was crippled by a knee injury." }, + end_node + }; + +--- +test: Newlines preserved for indented and blank lines +todo: true +spec: 2.15 +yaml: | + --- > + Sammy Sosa completed another + fine season with great stats. + + 63 Home Runs + 0.288 Batting Average + + What a year! +perl: | + "Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!\n" +ruby: | + "Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!\n" +python: | + [ + flushLeft( + """ + Sammy Sosa completed another fine season with great stats. + + 63 Home Runs + 0.288 Batting Average + + What a year! + """ + ) + ] +syck: | + struct test_node stream[] = { + { T_STR, 0, "Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!\n" }, + end_node + }; + + +--- +test: Indentation determines scope +spec: 2.16 +yaml: | + name: Mark McGwire + accomplishment: > + Mark set a major league + home run record in 1998. + stats: | + 65 Home Runs + 0.278 Batting Average +php: | + array( + 'name' => 'Mark McGwire', + 'accomplishment' => "Mark set a major league home run record in 1998.\n", + 'stats' => "65 Home Runs\n0.278 Batting Average\n" + ) +--- +test: Quoted scalars +todo: true +spec: 2.17 +yaml: | + unicode: "Sosa did fine.\u263A" + control: "\b1998\t1999\t2000\n" + hexesc: "\x0D\x0A is \r\n" + + single: '"Howdy!" he cried.' + quoted: ' # not a ''comment''.' + tie-fighter: '|\-*-/|' +ruby: | + { + "tie-fighter" => "|\\-*-/|", + "control"=>"\0101998\t1999\t2000\n", + "unicode"=>"Sosa did fine." + ["263A".hex ].pack('U*'), + "quoted"=>" # not a 'comment'.", + "single"=>"\"Howdy!\" he cried.", + "hexesc"=>"\r\n is \r\n" + } +--- +test: Multiline flow scalars +todo: true +spec: 2.18 +yaml: | + plain: + This unquoted scalar + spans many lines. + + quoted: "So does this + quoted scalar.\n" +ruby: | + { + 'plain' => 'This unquoted scalar spans many lines.', + 'quoted' => "So does this quoted scalar.\n" + } +--- +test: Integers +spec: 2.19 +yaml: | + canonical: 12345 + decimal: +12,345 + octal: 014 + hexadecimal: 0xC +php: | + array( + 'canonical' => 12345, + 'decimal' => 12345.0, + 'octal' => 014, + 'hexadecimal' => 0xC + ) +--- +# FIX: spec shows parens around -inf and NaN +test: Floating point +spec: 2.20 +yaml: | + canonical: 1.23015e+3 + exponential: 12.3015e+02 + fixed: 1,230.15 + negative infinity: -.inf + not a number: .NaN + float as whole number: !!float 1 +php: | + array( + 'canonical' => 1230.15, + 'exponential' => 1230.15, + 'fixed' => 1230.15, + 'negative infinity' => log(0), + 'not a number' => -log(0), + 'float as whole number' => (float) 1 + ) +--- +test: Miscellaneous +spec: 2.21 +yaml: | + null: ~ + true: true + false: false + string: '12345' +php: | + array( + '' => null, + 1 => true, + 0 => false, + 'string' => '12345' + ) +--- +test: Timestamps +todo: true +spec: 2.22 +yaml: | + canonical: 2001-12-15T02:59:43.1Z + iso8601: 2001-12-14t21:59:43.10-05:00 + spaced: 2001-12-14 21:59:43.10 -05:00 + date: 2002-12-14 # Time is noon UTC +php: | + array( + 'canonical' => YAML::mktime( 2001, 12, 15, 2, 59, 43, 0.10 ), + 'iso8601' => YAML::mktime( 2001, 12, 14, 21, 59, 43, 0.10, "-05:00" ), + 'spaced' => YAML::mktime( 2001, 12, 14, 21, 59, 43, 0.10, "-05:00" ), + 'date' => Date.new( 2002, 12, 14 ) + ) +--- +test: legacy Timestamps test +todo: true +spec: legacy D4 +yaml: | + canonical: 2001-12-15T02:59:43.00Z + iso8601: 2001-02-28t21:59:43.00-05:00 + spaced: 2001-12-14 21:59:43.00 -05:00 + date: 2002-12-14 +php: | + array( + 'canonical' => Time::utc( 2001, 12, 15, 2, 59, 43, 0 ), + 'iso8601' => YAML::mktime( 2001, 2, 28, 21, 59, 43, 0, "-05:00" ), + 'spaced' => YAML::mktime( 2001, 12, 14, 21, 59, 43, 0, "-05:00" ), + 'date' => Date.new( 2002, 12, 14 ) + ) +--- +test: Various explicit families +todo: true +spec: 2.23 +yaml: | + not-date: !str 2002-04-28 + picture: !binary | + R0lGODlhDAAMAIQAAP//9/X + 17unp5WZmZgAAAOfn515eXv + Pz7Y6OjuDg4J+fn5OTk6enp + 56enmleECcgggoBADs= + + application specific tag: !!something | + The semantics of the tag + above may be different for + different documents. + +ruby-setup: | + YAML.add_private_type( "something" ) do |type, val| + "SOMETHING: #{val}" + end +ruby: | + { + 'not-date' => '2002-04-28', + 'picture' => "GIF89a\f\000\f\000\204\000\000\377\377\367\365\365\356\351\351\345fff\000\000\000\347\347\347^^^\363\363\355\216\216\216\340\340\340\237\237\237\223\223\223\247\247\247\236\236\236i^\020' \202\n\001\000;", + 'application specific tag' => "SOMETHING: The semantics of the tag\nabove may be different for\ndifferent documents.\n" + } +--- +test: Application specific family +todo: true +spec: 2.24 +yaml: | + # Establish a tag prefix + --- !clarkevans.com,2002/graph/^shape + # Use the prefix: shorthand for + # !clarkevans.com,2002/graph/circle + - !^circle + center: &ORIGIN {x: 73, 'y': 129} + radius: 7 + - !^line # !clarkevans.com,2002/graph/line + start: *ORIGIN + finish: { x: 89, 'y': 102 } + - !^label + start: *ORIGIN + color: 0xFFEEBB + value: Pretty vector drawing. +ruby-setup: | + YAML.add_domain_type( "clarkevans.com,2002", 'graph/shape' ) { |type, val| + if Array === val + val << "Shape Container" + val + else + raise YAML::Error, "Invalid graph of class #{ val.class }: " + val.inspect + end + } + one_shape_proc = Proc.new { |type, val| + scheme, domain, type = type.split( /:/, 3 ) + if val.is_a? ::Hash + val['TYPE'] = "Shape: #{type}" + val + else + raise YAML::Error, "Invalid graph of class #{ val.class }: " + val.inspect + end + } + YAML.add_domain_type( "clarkevans.com,2002", 'graph/circle', &one_shape_proc ) + YAML.add_domain_type( "clarkevans.com,2002", 'graph/line', &one_shape_proc ) + YAML.add_domain_type( "clarkevans.com,2002", 'graph/label', &one_shape_proc ) +ruby: | + [ + { + "radius" => 7, + "center"=> + { + "x" => 73, + "y" => 129 + }, + "TYPE" => "Shape: graph/circle" + }, { + "finish" => + { + "x" => 89, + "y" => 102 + }, + "TYPE" => "Shape: graph/line", + "start" => + { + "x" => 73, + "y" => 129 + } + }, { + "TYPE" => "Shape: graph/label", + "value" => "Pretty vector drawing.", + "start" => + { + "x" => 73, + "y" => 129 + }, + "color" => 16772795 + }, + "Shape Container" + ] +# --- +# test: Unordered set +# spec: 2.25 +# yaml: | +# # sets are represented as a +# # mapping where each key is +# # associated with the empty string +# --- !set +# ? Mark McGwire +# ? Sammy Sosa +# ? Ken Griff +--- +test: Ordered mappings +todo: true +spec: 2.26 +yaml: | + # ordered maps are represented as + # a sequence of mappings, with + # each mapping having one key + --- !omap + - Mark McGwire: 65 + - Sammy Sosa: 63 + - Ken Griffy: 58 +ruby: | + YAML::Omap[ + 'Mark McGwire', 65, + 'Sammy Sosa', 63, + 'Ken Griffy', 58 + ] +--- +test: Invoice +dump_skip: true +spec: 2.27 +yaml: | + --- !clarkevans.com,2002/^invoice + invoice: 34843 + date : 2001-01-23 + bill-to: &id001 + given : Chris + family : Dumars + address: + lines: | + 458 Walkman Dr. + Suite #292 + city : Royal Oak + state : MI + postal : 48046 + ship-to: *id001 + product: + - + sku : BL394D + quantity : 4 + description : Basketball + price : 450.00 + - + sku : BL4438H + quantity : 1 + description : Super Hoop + price : 2392.00 + tax : 251.42 + total: 4443.52 + comments: > + Late afternoon is best. + Backup contact is Nancy + Billsmer @ 338-4338. +php: | + array( + 'invoice' => 34843, 'date' => gmmktime(0, 0, 0, 1, 23, 2001), + 'bill-to' => + array( 'given' => 'Chris', 'family' => 'Dumars', 'address' => array( 'lines' => "458 Walkman Dr.\nSuite #292\n", 'city' => 'Royal Oak', 'state' => 'MI', 'postal' => 48046 ) ) + , 'ship-to' => + array( 'given' => 'Chris', 'family' => 'Dumars', 'address' => array( 'lines' => "458 Walkman Dr.\nSuite #292\n", 'city' => 'Royal Oak', 'state' => 'MI', 'postal' => 48046 ) ) + , 'product' => + array( + array( 'sku' => 'BL394D', 'quantity' => 4, 'description' => 'Basketball', 'price' => 450.00 ), + array( 'sku' => 'BL4438H', 'quantity' => 1, 'description' => 'Super Hoop', 'price' => 2392.00 ) + ), + 'tax' => 251.42, 'total' => 4443.52, + 'comments' => "Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338.\n" + ) +--- +test: Log file +todo: true +spec: 2.28 +yaml: | + --- + Time: 2001-11-23 15:01:42 -05:00 + User: ed + Warning: > + This is an error message + for the log file + --- + Time: 2001-11-23 15:02:31 -05:00 + User: ed + Warning: > + A slightly different error + message. + --- + Date: 2001-11-23 15:03:17 -05:00 + User: ed + Fatal: > + Unknown variable "bar" + Stack: + - file: TopClass.py + line: 23 + code: | + x = MoreObject("345\n") + - file: MoreClass.py + line: 58 + code: |- + foo = bar +ruby: | + y = YAML::Stream.new + y.add( { 'Time' => YAML::mktime( 2001, 11, 23, 15, 01, 42, 00, "-05:00" ), + 'User' => 'ed', 'Warning' => "This is an error message for the log file\n" } ) + y.add( { 'Time' => YAML::mktime( 2001, 11, 23, 15, 02, 31, 00, "-05:00" ), + 'User' => 'ed', 'Warning' => "A slightly different error message.\n" } ) + y.add( { 'Date' => YAML::mktime( 2001, 11, 23, 15, 03, 17, 00, "-05:00" ), + 'User' => 'ed', 'Fatal' => "Unknown variable \"bar\"\n", + 'Stack' => [ + { 'file' => 'TopClass.py', 'line' => 23, 'code' => "x = MoreObject(\"345\\n\")\n" }, + { 'file' => 'MoreClass.py', 'line' => 58, 'code' => "foo = bar" } ] } ) +documents: 3 + +--- +test: Throwaway comments +yaml: | + ### These are four throwaway comment ### + + ### lines (the second line is empty). ### + this: | # Comments may trail lines. + contains three lines of text. + The third one starts with a + # character. This isn't a comment. + + # These are three throwaway comment + # lines (the first line is empty). +php: | + array( + 'this' => "contains three lines of text.\nThe third one starts with a\n# character. This isn't a comment.\n" + ) +--- +test: Document with a single value +todo: true +yaml: | + --- > + This YAML stream contains a single text value. + The next stream is a log file - a sequence of + log entries. Adding an entry to the log is a + simple matter of appending it at the end. +ruby: | + "This YAML stream contains a single text value. The next stream is a log file - a sequence of log entries. Adding an entry to the log is a simple matter of appending it at the end.\n" +--- +test: Document stream +todo: true +yaml: | + --- + at: 2001-08-12 09:25:00.00 Z + type: GET + HTTP: '1.0' + url: '/index.html' + --- + at: 2001-08-12 09:25:10.00 Z + type: GET + HTTP: '1.0' + url: '/toc.html' +ruby: | + y = YAML::Stream.new + y.add( { + 'at' => Time::utc( 2001, 8, 12, 9, 25, 00 ), + 'type' => 'GET', + 'HTTP' => '1.0', + 'url' => '/index.html' + } ) + y.add( { + 'at' => Time::utc( 2001, 8, 12, 9, 25, 10 ), + 'type' => 'GET', + 'HTTP' => '1.0', + 'url' => '/toc.html' + } ) +documents: 2 + +--- +test: Top level mapping +yaml: | + # This stream is an example of a top-level mapping. + invoice : 34843 + date : 2001-01-23 + total : 4443.52 +php: | + array( + 'invoice' => 34843, + 'date' => gmmktime(0, 0, 0, 1, 23, 2001), + 'total' => 4443.52 + ) +--- +test: Single-line documents +todo: true +yaml: | + # The following is a sequence of three documents. + # The first contains an empty mapping, the second + # an empty sequence, and the last an empty string. + --- {} + --- [ ] + --- '' +ruby: | + y = YAML::Stream.new + y.add( {} ) + y.add( [] ) + y.add( '' ) +documents: 3 + +--- +test: Document with pause +todo: true +yaml: | + # A communication channel based on a YAML stream. + --- + sent at: 2002-06-06 11:46:25.10 Z + payload: Whatever + # Receiver can process this as soon as the following is sent: + ... + # Even if the next message is sent long after: + --- + sent at: 2002-06-06 12:05:53.47 Z + payload: Whatever + ... +ruby: | + y = YAML::Stream.new + y.add( + { 'sent at' => YAML::mktime( 2002, 6, 6, 11, 46, 25, 0.10 ), + 'payload' => 'Whatever' } + ) + y.add( + { "payload" => "Whatever", "sent at" => YAML::mktime( 2002, 6, 6, 12, 5, 53, 0.47 ) } + ) +documents: 2 + +--- +test: Explicit typing +yaml: | + integer: 12 + also int: ! "12" + string: !str 12 +php: | + array( 'integer' => 12, 'also int' => 12, 'string' => '12' ) +--- +test: Private types +todo: true +yaml: | + # Both examples below make use of the 'x-private:ball' + # type family URI, but with different semantics. + --- + pool: !!ball + number: 8 + color: black + --- + bearing: !!ball + material: steel +ruby: | + y = YAML::Stream.new + y.add( { 'pool' => + YAML::PrivateType.new( 'ball', + { 'number' => 8, 'color' => 'black' } ) } + ) + y.add( { 'bearing' => + YAML::PrivateType.new( 'ball', + { 'material' => 'steel' } ) } + ) +documents: 2 + +--- +test: Type family under yaml.org +yaml: | + # The URI is 'tag:yaml.org,2002:str' + - !str a Unicode string +php: | + array( 'a Unicode string' ) +--- +test: Type family under perl.yaml.org +todo: true +yaml: | + # The URI is 'tag:perl.yaml.org,2002:Text::Tabs' + - !perl/Text::Tabs {} +ruby: | + [ YAML::DomainType.new( 'perl.yaml.org,2002', 'Text::Tabs', {} ) ] +--- +test: Type family under clarkevans.com +todo: true +yaml: | + # The URI is 'tag:clarkevans.com,2003-02:timesheet' + - !clarkevans.com,2003-02/timesheet {} +ruby: | + [ YAML::DomainType.new( 'clarkevans.com,2003-02', 'timesheet', {} ) ] +--- +test: URI Escaping +todo: true +yaml: | + same: + - !domain.tld,2002/type\x30 value + - !domain.tld,2002/type0 value + different: # As far as the YAML parser is concerned + - !domain.tld,2002/type%30 value + - !domain.tld,2002/type0 value +ruby-setup: | + YAML.add_domain_type( "domain.tld,2002", "type0" ) { |type, val| + "ONE: #{val}" + } + YAML.add_domain_type( "domain.tld,2002", "type%30" ) { |type, val| + "TWO: #{val}" + } +ruby: | + { 'same' => [ 'ONE: value', 'ONE: value' ], 'different' => [ 'TWO: value', 'ONE: value' ] } +--- +test: URI Prefixing +todo: true +yaml: | + # 'tag:domain.tld,2002:invoice' is some type family. + invoice: !domain.tld,2002/^invoice + # 'seq' is shorthand for 'tag:yaml.org,2002:seq'. + # This does not effect '^customer' below + # because it is does not specify a prefix. + customers: !seq + # '^customer' is shorthand for the full + # notation 'tag:domain.tld,2002:customer'. + - !^customer + given : Chris + family : Dumars +ruby-setup: | + YAML.add_domain_type( "domain.tld,2002", /(invoice|customer)/ ) { |type, val| + if val.is_a? ::Hash + scheme, domain, type = type.split( /:/, 3 ) + val['type'] = "domain #{type}" + val + else + raise YAML::Error, "Not a Hash in domain.tld/invoice: " + val.inspect + end + } +ruby: | + { "invoice"=> { "customers"=> [ { "given"=>"Chris", "type"=>"domain customer", "family"=>"Dumars" } ], "type"=>"domain invoice" } } + +--- +test: Overriding anchors +yaml: | + anchor : &A001 This scalar has an anchor. + override : &A001 > + The alias node below is a + repeated use of this value. + alias : *A001 +php: | + array( 'anchor' => 'This scalar has an anchor.', + 'override' => "The alias node below is a repeated use of this value.\n", + 'alias' => "The alias node below is a repeated use of this value.\n" ) +--- +test: Flow and block formatting +todo: true +yaml: | + empty: [] + flow: [ one, two, three # May span lines, + , four, # indentation is + five ] # mostly ignored. + block: + - First item in top sequence + - + - Subordinate sequence entry + - > + A folded sequence entry + - Sixth item in top sequence +ruby: | + { 'empty' => [], 'flow' => [ 'one', 'two', 'three', 'four', 'five' ], + 'block' => [ 'First item in top sequence', [ 'Subordinate sequence entry' ], + "A folded sequence entry\n", 'Sixth item in top sequence' ] } +--- +test: Complete mapping test +todo: true +yaml: | + empty: {} + flow: { one: 1, two: 2 } + spanning: { one: 1, + two: 2 } + block: + first : First entry + second: + key: Subordinate mapping + third: + - Subordinate sequence + - { } + - Previous mapping is empty. + - A key: value pair in a sequence. + A second: key:value pair. + - The previous entry is equal to the following one. + - + A key: value pair in a sequence. + A second: key:value pair. + !float 12 : This key is a float. + ? > + ? + : This key had to be protected. + "\a" : This key had to be escaped. + ? > + This is a + multi-line + folded key + : Whose value is + also multi-line. + ? this also works as a key + : with a value at the next line. + ? + - This key + - is a sequence + : + - With a sequence value. + ? + This: key + is a: mapping + : + with a: mapping value. +ruby: | + { 'empty' => {}, 'flow' => { 'one' => 1, 'two' => 2 }, + 'spanning' => { 'one' => 1, 'two' => 2 }, + 'block' => { 'first' => 'First entry', 'second' => + { 'key' => 'Subordinate mapping' }, 'third' => + [ 'Subordinate sequence', {}, 'Previous mapping is empty.', + { 'A key' => 'value pair in a sequence.', 'A second' => 'key:value pair.' }, + 'The previous entry is equal to the following one.', + { 'A key' => 'value pair in a sequence.', 'A second' => 'key:value pair.' } ], + 12.0 => 'This key is a float.', "?\n" => 'This key had to be protected.', + "\a" => 'This key had to be escaped.', + "This is a multi-line folded key\n" => "Whose value is also multi-line.", + 'this also works as a key' => 'with a value at the next line.', + [ 'This key', 'is a sequence' ] => [ 'With a sequence value.' ] } } + # Couldn't recreate map exactly, so we'll do a detailed check to be sure it's entact + obj_y['block'].keys.each { |k| + if Hash === k + v = obj_y['block'][k] + if k['This'] == 'key' and k['is a'] == 'mapping' and v['with a'] == 'mapping value.' + obj_r['block'][k] = v + end + end + } +--- +test: Literal explicit indentation +yaml: | + # Explicit indentation must + # be given in all the three + # following cases. + leading spaces: |2 + This value starts with four spaces. + + leading line break: |2 + + This value starts with a line break. + + leading comment indicator: |2 + # first line starts with a + # character. + + # Explicit indentation may + # also be given when it is + # not required. + redundant: |2 + This value is indented 2 spaces. +php: | + array( + 'leading spaces' => " This value starts with four spaces.\n", + 'leading line break' => "\nThis value starts with a line break.\n", + 'leading comment indicator' => "# first line starts with a\n# character.\n", + 'redundant' => "This value is indented 2 spaces.\n" + ) +--- +test: Chomping and keep modifiers +yaml: | + clipped: | + This has one newline. + + same as "clipped" above: "This has one newline.\n" + + stripped: |- + This has no newline. + + same as "stripped" above: "This has no newline." + + kept: |+ + This has two newlines. + + same as "kept" above: "This has two newlines.\n\n" +php: | + array( + 'clipped' => "This has one newline.\n", + 'same as "clipped" above' => "This has one newline.\n", + 'stripped' => 'This has no newline.', + 'same as "stripped" above' => 'This has no newline.', + 'kept' => "This has two newlines.\n\n", + 'same as "kept" above' => "This has two newlines.\n\n" + ) +--- +test: Literal combinations +todo: true +yaml: | + empty: | + + literal: | + The \ ' " characters may be + freely used. Leading white + space is significant. + + Line breaks are significant. + Thus this value contains one + empty line and ends with a + single line break, but does + not start with one. + + is equal to: "The \\ ' \" characters may \ + be\nfreely used. Leading white\n space \ + is significant.\n\nLine breaks are \ + significant.\nThus this value contains \ + one\nempty line and ends with a\nsingle \ + line break, but does\nnot start with one.\n" + + # Comments may follow a block + # scalar value. They must be + # less indented. + + # Modifiers may be combined in any order. + indented and chomped: |2- + This has no newline. + + also written as: |-2 + This has no newline. + + both are equal to: " This has no newline." +php: | + array( + 'empty' => '', + 'literal' => "The \\ ' \" characters may be\nfreely used. Leading white\n space " + + "is significant.\n\nLine breaks are significant.\nThus this value contains one\n" + + "empty line and ends with a\nsingle line break, but does\nnot start with one.\n", + 'is equal to' => "The \\ ' \" characters may be\nfreely used. Leading white\n space " + + "is significant.\n\nLine breaks are significant.\nThus this value contains one\n" + + "empty line and ends with a\nsingle line break, but does\nnot start with one.\n", + 'indented and chomped' => ' This has no newline.', + 'also written as' => ' This has no newline.', + 'both are equal to' => ' This has no newline.' + ) +--- +test: Folded combinations +todo: true +yaml: | + empty: > + + one paragraph: > + Line feeds are converted + to spaces, so this value + contains no line breaks + except for the final one. + + multiple paragraphs: >2 + + An empty line, either + at the start or in + the value: + + Is interpreted as a + line break. Thus this + value contains three + line breaks. + + indented text: > + This is a folded + paragraph followed + by a list: + * first entry + * second entry + Followed by another + folded paragraph, + another list: + + * first entry + + * second entry + + And a final folded + paragraph. + + above is equal to: | + This is a folded paragraph followed by a list: + * first entry + * second entry + Followed by another folded paragraph, another list: + + * first entry + + * second entry + + And a final folded paragraph. + + # Explicit comments may follow + # but must be less indented. +php: | + array( + 'empty' => '', + 'one paragraph' => 'Line feeds are converted to spaces, so this value'. + " contains no line breaks except for the final one.\n", + 'multiple paragraphs' => "\nAn empty line, either at the start or in the value:\n". + "Is interpreted as a line break. Thus this value contains three line breaks.\n", + 'indented text' => "This is a folded paragraph followed by a list:\n". + " * first entry\n * second entry\nFollowed by another folded paragraph, ". + "another list:\n\n * first entry\n\n * second entry\n\nAnd a final folded paragraph.\n", + 'above is equal to' => "This is a folded paragraph followed by a list:\n". + " * first entry\n * second entry\nFollowed by another folded paragraph, ". + "another list:\n\n * first entry\n\n * second entry\n\nAnd a final folded paragraph.\n" + ) +--- +test: Single quotes +todo: true +yaml: | + empty: '' + second: '! : \ etc. can be used freely.' + third: 'a single quote '' must be escaped.' + span: 'this contains + six spaces + + and one + line break' + is same as: "this contains six spaces\nand one line break" +php: | + array( + 'empty' => '', + 'second' => '! : \\ etc. can be used freely.', + 'third' => "a single quote ' must be escaped.", + 'span' => "this contains six spaces\nand one line break", + 'is same as' => "this contains six spaces\nand one line break" + ) +--- +test: Double quotes +todo: true +yaml: | + empty: "" + second: "! : etc. can be used freely." + third: "a \" or a \\ must be escaped." + fourth: "this value ends with an LF.\n" + span: "this contains + four \ + spaces" + is equal to: "this contains four spaces" +php: | + array( + 'empty' => '', + 'second' => '! : etc. can be used freely.', + 'third' => 'a " or a \\ must be escaped.', + 'fourth' => "this value ends with an LF.\n", + 'span' => "this contains four spaces", + 'is equal to' => "this contains four spaces" + ) +--- +test: Unquoted strings +todo: true +yaml: | + first: There is no unquoted empty string. + + second: 12 ## This is an integer. + + third: !str 12 ## This is a string. + + span: this contains + six spaces + + and one + line break + + indicators: this has no comments. + #:foo and bar# are + both text. + + flow: [ can span + lines, # comment + like + this ] + + note: { one-line keys: but multi-line values } + +php: | + array( + 'first' => 'There is no unquoted empty string.', + 'second' => 12, + 'third' => '12', + 'span' => "this contains six spaces\nand one line break", + 'indicators' => "this has no comments. #:foo and bar# are both text.", + 'flow' => [ 'can span lines', 'like this' ], + 'note' => { 'one-line keys' => 'but multi-line values' } + ) +--- +test: Spanning sequences +todo: true +yaml: | + # The following are equal seqs + # with different identities. + flow: [ one, two ] + spanning: [ one, + two ] + block: + - one + - two +php: | + array( + 'flow' => [ 'one', 'two' ], + 'spanning' => [ 'one', 'two' ], + 'block' => [ 'one', 'two' ] + ) +--- +test: Flow mappings +yaml: | + # The following are equal maps + # with different identities. + flow: { one: 1, two: 2 } + block: + one: 1 + two: 2 +php: | + array( + 'flow' => array( 'one' => 1, 'two' => 2 ), + 'block' => array( 'one' => 1, 'two' => 2 ) + ) +--- +test: Representations of 12 +todo: true +yaml: | + - 12 # An integer + # The following scalars + # are loaded to the + # string value '1' '2'. + - !str 12 + - '12' + - "12" + - "\ + 1\ + 2\ + " + # Strings containing paths and regexps can be unquoted: + - /foo/bar + - d:/foo/bar + - foo/bar + - /a.*b/ +php: | + array( 12, '12', '12', '12', '12', '/foo/bar', 'd:/foo/bar', 'foo/bar', '/a.*b/' ) +--- +test: "Null" +todo: true +yaml: | + canonical: ~ + + english: null + + # This sequence has five + # entries, two with values. + sparse: + - ~ + - 2nd entry + - Null + - 4th entry + - + + four: This mapping has five keys, + only two with values. + +php: | + array ( + 'canonical' => null, + 'english' => null, + 'sparse' => array( null, '2nd entry', null, '4th entry', null ]), + 'four' => 'This mapping has five keys, only two with values.' + ) +--- +test: Omap +todo: true +yaml: | + # Explicitly typed dictionary. + Bestiary: !omap + - aardvark: African pig-like ant eater. Ugly. + - anteater: South-American ant eater. Two species. + - anaconda: South-American constrictor snake. Scary. + # Etc. +ruby: | + { + 'Bestiary' => YAML::Omap[ + 'aardvark', 'African pig-like ant eater. Ugly.', + 'anteater', 'South-American ant eater. Two species.', + 'anaconda', 'South-American constrictor snake. Scary.' + ] + } + +--- +test: Pairs +todo: true +yaml: | + # Explicitly typed pairs. + tasks: !pairs + - meeting: with team. + - meeting: with boss. + - break: lunch. + - meeting: with client. +ruby: | + { + 'tasks' => YAML::Pairs[ + 'meeting', 'with team.', + 'meeting', 'with boss.', + 'break', 'lunch.', + 'meeting', 'with client.' + ] + } + +--- +test: Set +todo: true +yaml: | + # Explicitly typed set. + baseball players: !set + Mark McGwire: + Sammy Sosa: + Ken Griffey: +ruby: | + { + 'baseball players' => YAML::Set[ + 'Mark McGwire', nil, + 'Sammy Sosa', nil, + 'Ken Griffey', nil + ] + } + +--- +test: Boolean +yaml: | + false: used as key + logical: true + answer: false +php: | + array( + false => 'used as key', + 'logical' => true, + 'answer' => false + ) +--- +test: Integer +yaml: | + canonical: 12345 + decimal: +12,345 + octal: 014 + hexadecimal: 0xC +php: | + array( + 'canonical' => 12345, + 'decimal' => 12345.0, + 'octal' => 12, + 'hexadecimal' => 12 + ) +--- +test: Float +yaml: | + canonical: 1.23015e+3 + exponential: 12.3015e+02 + fixed: 1,230.15 + negative infinity: -.inf + not a number: .NaN +php: | + array( + 'canonical' => 1230.15, + 'exponential' => 1230.15, + 'fixed' => 1230.15, + 'negative infinity' => log(0), + 'not a number' => -log(0) + ) +--- +test: Timestamp +todo: true +yaml: | + canonical: 2001-12-15T02:59:43.1Z + valid iso8601: 2001-12-14t21:59:43.10-05:00 + space separated: 2001-12-14 21:59:43.10 -05:00 + date (noon UTC): 2002-12-14 +ruby: | + array( + 'canonical' => YAML::mktime( 2001, 12, 15, 2, 59, 43, 0.10 ), + 'valid iso8601' => YAML::mktime( 2001, 12, 14, 21, 59, 43, 0.10, "-05:00" ), + 'space separated' => YAML::mktime( 2001, 12, 14, 21, 59, 43, 0.10, "-05:00" ), + 'date (noon UTC)' => Date.new( 2002, 12, 14 ) + ) +--- +test: Binary +todo: true +yaml: | + canonical: !binary "\ + R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5\ + OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+\ + +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC\ + AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=" + base64: !binary | + R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5 + OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+ + +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC + AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs= + description: > + The binary value above is a tiny arrow + encoded as a gif image. +ruby-setup: | + arrow_gif = "GIF89a\f\000\f\000\204\000\000\377\377\367\365\365\356\351\351\345fff\000\000\000\347\347\347^^^\363\363\355\216\216\216\340\340\340\237\237\237\223\223\223\247\247\247\236\236\236iiiccc\243\243\243\204\204\204\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371!\376\016Made with GIMP\000,\000\000\000\000\f\000\f\000\000\005, \216\2010\236\343@\024\350i\020\304\321\212\010\034\317\200M$z\357\3770\205p\270\2601f\r\e\316\001\303\001\036\020' \202\n\001\000;" +ruby: | + { + 'canonical' => arrow_gif, + 'base64' => arrow_gif, + 'description' => "The binary value above is a tiny arrow encoded as a gif image.\n" + } + +--- +test: Merge key +todo: true +yaml: | + --- + - &CENTER { x: 1, y: 2 } + - &LEFT { x: 0, y: 2 } + - &BIG { r: 10 } + - &SMALL { r: 1 } + + # All the following maps are equal: + + - # Explicit keys + x: 1 + y: 2 + r: 10 + label: center/big + + - # Merge one map + << : *CENTER + r: 10 + label: center/big + + - # Merge multiple maps + << : [ *CENTER, *BIG ] + label: center/big + + - # Override + << : [ *BIG, *LEFT, *SMALL ] + x: 1 + label: center/big + +ruby-setup: | + center = { 'x' => 1, 'y' => 2 } + left = { 'x' => 0, 'y' => 2 } + big = { 'r' => 10 } + small = { 'r' => 1 } + node1 = { 'x' => 1, 'y' => 2, 'r' => 10, 'label' => 'center/big' } + node2 = center.dup + node2.update( { 'r' => 10, 'label' => 'center/big' } ) + node3 = big.dup + node3.update( center ) + node3.update( { 'label' => 'center/big' } ) + node4 = small.dup + node4.update( left ) + node4.update( big ) + node4.update( { 'x' => 1, 'label' => 'center/big' } ) + +ruby: | + [ + center, left, big, small, node1, node2, node3, node4 + ] + +--- +test: Default key +todo: true +yaml: | + --- # Old schema + link with: + - library1.dll + - library2.dll + --- # New schema + link with: + - = : library1.dll + version: 1.2 + - = : library2.dll + version: 2.3 +ruby: | + y = YAML::Stream.new + y.add( { 'link with' => [ 'library1.dll', 'library2.dll' ] } ) + obj_h = Hash[ 'version' => 1.2 ] + obj_h.default = 'library1.dll' + obj_h2 = Hash[ 'version' => 2.3 ] + obj_h2.default = 'library2.dll' + y.add( { 'link with' => [ obj_h, obj_h2 ] } ) +documents: 2 + +--- +test: Special keys +todo: true +yaml: | + "!": These three keys + "&": had to be quoted + "=": and are normal strings. + # NOTE: the following node should NOT be serialized this way. + encoded node : + !special '!' : '!type' + !special|canonical '&' : 12 + = : value + # The proper way to serialize the above node is as follows: + node : !!type &12 value +ruby: | + { '!' => 'These three keys', '&' => 'had to be quoted', + '=' => 'and are normal strings.', + 'encoded node' => YAML::PrivateType.new( 'type', 'value' ), + 'node' => YAML::PrivateType.new( 'type', 'value' ) } diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsTypeTransfers.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsTypeTransfers.yml new file mode 100644 index 0000000..46c8d4a --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/YtsTypeTransfers.yml @@ -0,0 +1,244 @@ +--- %YAML:1.0 +test: Strings +brief: > + Any group of characters beginning with an + alphabetic or numeric character is a string, + unless it belongs to one of the groups below + (such as an Integer or Time). +yaml: | + String +php: | + 'String' +--- +test: String characters +brief: > + A string can contain any alphabetic or + numeric character, along with many + punctuation characters, including the + period, dash, space, quotes, exclamation, and + question mark. +yaml: | + - What's Yaml? + - It's for writing data structures in plain text. + - And? + - And what? That's not good enough for you? + - No, I mean, "And what about Yaml?" + - Oh, oh yeah. Uh.. Yaml for Ruby. +php: | + array( + "What's Yaml?", + "It's for writing data structures in plain text.", + "And?", + "And what? That's not good enough for you?", + "No, I mean, \"And what about Yaml?\"", + "Oh, oh yeah. Uh.. Yaml for Ruby." + ) +--- +test: Indicators in Strings +brief: > + Be careful using indicators in strings. In particular, + the comma, colon, and pound sign must be used carefully. +yaml: | + the colon followed by space is an indicator: but is a string:right here + same for the pound sign: here we have it#in a string + the comma can, honestly, be used in most cases: [ but not in, inline collections ] +php: | + array( + 'the colon followed by space is an indicator' => 'but is a string:right here', + 'same for the pound sign' => 'here we have it#in a string', + 'the comma can, honestly, be used in most cases' => array('but not in', 'inline collections') + ) +--- +test: Forcing Strings +brief: > + Any YAML type can be forced into a string using the + explicit !str method. +yaml: | + date string: !str 2001-08-01 + number string: !str 192 +php: | + array( + 'date string' => '2001-08-01', + 'number string' => '192' + ) +--- +test: Single-quoted Strings +brief: > + You can also enclose your strings within single quotes, + which allows use of slashes, colons, and other indicators + freely. Inside single quotes, you can represent a single + quote in your string by using two single quotes next to + each other. +yaml: | + all my favorite symbols: '#:!/%.)' + a few i hate: '&(*' + why do i hate them?: 'it''s very hard to explain' + entities: '£ me' +php: | + array( + 'all my favorite symbols' => '#:!/%.)', + 'a few i hate' => '&(*', + 'why do i hate them?' => 'it\'s very hard to explain', + 'entities' => '£ me' + ) +--- +test: Double-quoted Strings +brief: > + Enclosing strings in double quotes allows you + to use escapings to represent ASCII and + Unicode characters. +yaml: | + i know where i want my line breaks: "one here\nand another here\n" +php: | + array( + 'i know where i want my line breaks' => "one here\nand another here\n" + ) +--- +test: Multi-line Quoted Strings +todo: true +brief: > + Both single- and double-quoted strings may be + carried on to new lines in your YAML document. + They must be indented a step and indentation + is interpreted as a single space. +yaml: | + i want a long string: "so i'm going to + let it go on and on to other lines + until i end it with a quote." +php: | + array('i want a long string' => "so i'm going to ". + "let it go on and on to other lines ". + "until i end it with a quote." + ) + +--- +test: Plain scalars +todo: true +brief: > + Unquoted strings may also span multiple lines, if they + are free of YAML space indicators and indented. +yaml: | + - My little toe is broken in two places; + - I'm crazy to have skied this way; + - I'm not the craziest he's seen, since there was always the German guy + who skied for 3 hours on a broken shin bone (just below the kneecap); + - Nevertheless, second place is respectable, and he doesn't + recommend going for the record; + - He's going to put my foot in plaster for a month; + - This would impair my skiing ability somewhat for the + duration, as can be imagined. +php: | + array( + "My little toe is broken in two places;", + "I'm crazy to have skied this way;", + "I'm not the craziest he's seen, since there was always ". + "the German guy who skied for 3 hours on a broken shin ". + "bone (just below the kneecap);", + "Nevertheless, second place is respectable, and he doesn't ". + "recommend going for the record;", + "He's going to put my foot in plaster for a month;", + "This would impair my skiing ability somewhat for the duration, ". + "as can be imagined." + ) +--- +test: 'Null' +brief: > + You can use the tilde '~' character for a null value. +yaml: | + name: Mr. Show + hosted by: Bob and David + date of next season: ~ +php: | + array( + 'name' => 'Mr. Show', + 'hosted by' => 'Bob and David', + 'date of next season' => null + ) +--- +test: Boolean +brief: > + You can use 'true' and 'false' for Boolean values. +yaml: | + Is Gus a Liar?: true + Do I rely on Gus for Sustenance?: false +php: | + array( + 'Is Gus a Liar?' => true, + 'Do I rely on Gus for Sustenance?' => false + ) +--- +test: Integers +dump_skip: true +brief: > + An integer is a series of numbers, optionally + starting with a positive or negative sign. Integers + may also contain commas for readability. +yaml: | + zero: 0 + simple: 12 + one-thousand: 1,000 + negative one-thousand: -1,000 +php: | + array( + 'zero' => 0, + 'simple' => 12, + 'one-thousand' => 1000.0, + 'negative one-thousand' => -1000.0 + ) +--- +test: Integers as Map Keys +brief: > + An integer can be used a dictionary key. +yaml: | + 1: one + 2: two + 3: three +php: | + array( + 1 => 'one', + 2 => 'two', + 3 => 'three' + ) +--- +test: Floats +dump_skip: true +brief: > + Floats are represented by numbers with decimals, + allowing for scientific notation, as well as + positive and negative infinity and "not a number." +yaml: | + a simple float: 2.00 + larger float: 1,000.09 + scientific notation: 1.00009e+3 +php: | + array( + 'a simple float' => 2.0, + 'larger float' => 1000.09, + 'scientific notation' => 1000.09 + ) +--- +test: Time +todo: true +brief: > + You can represent timestamps by using + ISO8601 format, or a variation which + allows spaces between the date, time and + time zone. +yaml: | + iso8601: 2001-12-14t21:59:43.10-05:00 + space separated: 2001-12-14 21:59:43.10 -05:00 +php: | + array( + 'iso8601' => mktime( 2001, 12, 14, 21, 59, 43, 0.10, "-05:00" ), + 'space separated' => mktime( 2001, 12, 14, 21, 59, 43, 0.10, "-05:00" ) + ) +--- +test: Date +todo: true +brief: > + A date can be represented by its year, + month and day in ISO8601 order. +yaml: | + 1976-07-31 +php: | + date( 1976, 7, 31 ) diff --git a/vendor/symfony/yaml/Tests/Fixtures/embededPhp.yml b/vendor/symfony/yaml/Tests/Fixtures/embededPhp.yml new file mode 100644 index 0000000..ec456ed --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/embededPhp.yml @@ -0,0 +1 @@ +value: diff --git a/vendor/symfony/yaml/Tests/Fixtures/escapedCharacters.yml b/vendor/symfony/yaml/Tests/Fixtures/escapedCharacters.yml new file mode 100644 index 0000000..6ca044c --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/escapedCharacters.yml @@ -0,0 +1,155 @@ +test: outside double quotes +yaml: | + \0 \ \a \b \n +php: | + "\\0 \\ \\a \\b \\n" +--- +test: null +yaml: | + "\0" +php: | + "\x00" +--- +test: bell +yaml: | + "\a" +php: | + "\x07" +--- +test: backspace +yaml: | + "\b" +php: | + "\x08" +--- +test: horizontal tab (1) +yaml: | + "\t" +php: | + "\x09" +--- +test: horizontal tab (2) +yaml: | + "\ " +php: | + "\x09" +--- +test: line feed +yaml: | + "\n" +php: | + "\x0a" +--- +test: vertical tab +yaml: | + "\v" +php: | + "\x0b" +--- +test: form feed +yaml: | + "\f" +php: | + "\x0c" +--- +test: carriage return +yaml: | + "\r" +php: | + "\x0d" +--- +test: escape +yaml: | + "\e" +php: | + "\x1b" +--- +test: space +yaml: | + "\ " +php: | + "\x20" +--- +test: slash +yaml: | + "\/" +php: | + "\x2f" +--- +test: backslash +yaml: | + "\\" +php: | + "\\" +--- +test: Unicode next line +yaml: | + "\N" +php: | + "\xc2\x85" +--- +test: Unicode non-breaking space +yaml: | + "\_" +php: | + "\xc2\xa0" +--- +test: Unicode line separator +yaml: | + "\L" +php: | + "\xe2\x80\xa8" +--- +test: Unicode paragraph separator +yaml: | + "\P" +php: | + "\xe2\x80\xa9" +--- +test: Escaped 8-bit Unicode +yaml: | + "\x42" +php: | + "B" +--- +test: Escaped 16-bit Unicode +yaml: | + "\u20ac" +php: | + "\xe2\x82\xac" +--- +test: Escaped 32-bit Unicode +yaml: | + "\U00000043" +php: | + "C" +--- +test: Example 5.13 Escaped Characters +note: | + Currently throws an error parsing first line. Maybe Symfony Yaml doesn't support + continuation of string across multiple lines? Keeping test here but disabled. +todo: true +yaml: | + "Fun with \\ + \" \a \b \e \f \ + \n \r \t \v \0 \ + \ \_ \N \L \P \ + \x41 \u0041 \U00000041" +php: | + "Fun with \x5C\n\x22 \x07 \x08 \x1B \x0C\n\x0A \x0D \x09 \x0B \x00\n\x20 \xA0 \x85 \xe2\x80\xa8 \xe2\x80\xa9\nA A A" +--- +test: Double quotes with a line feed +yaml: | + { double: "some value\n \"some quoted string\" and 'some single quotes one'" } +php: | + array( + 'double' => "some value\n \"some quoted string\" and 'some single quotes one'" + ) +--- +test: Backslashes +yaml: | + { single: 'foo\Var', no-quotes: foo\Var, double: "foo\\Var" } +php: | + array( + 'single' => 'foo\Var', 'no-quotes' => 'foo\Var', 'double' => 'foo\Var' + ) diff --git a/vendor/symfony/yaml/Tests/Fixtures/index.yml b/vendor/symfony/yaml/Tests/Fixtures/index.yml new file mode 100644 index 0000000..3216a89 --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/index.yml @@ -0,0 +1,18 @@ +- escapedCharacters +- sfComments +- sfCompact +- sfTests +- sfObjects +- sfMergeKey +- sfQuotes +- YtsAnchorAlias +- YtsBasicTests +- YtsBlockMapping +- YtsDocumentSeparator +- YtsErrorTests +- YtsFlowCollections +- YtsFoldedScalars +- YtsNullsAndEmpties +- YtsSpecificationExamples +- YtsTypeTransfers +- unindentedCollections diff --git a/vendor/symfony/yaml/Tests/Fixtures/sfComments.yml b/vendor/symfony/yaml/Tests/Fixtures/sfComments.yml new file mode 100644 index 0000000..b72a9b6 --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/sfComments.yml @@ -0,0 +1,76 @@ +--- %YAML:1.0 +test: Comments at the end of a line +brief: > + Comments at the end of a line +yaml: | + ex1: "foo # bar" + ex2: "foo # bar" # comment + ex3: 'foo # bar' # comment + ex4: foo # comment + ex5: foo # comment with tab before + ex6: foo#foo # comment here + ex7: foo # ignore me # and me +php: | + array('ex1' => 'foo # bar', 'ex2' => 'foo # bar', 'ex3' => 'foo # bar', 'ex4' => 'foo', 'ex5' => 'foo', 'ex6' => 'foo#foo', 'ex7' => 'foo') +--- +test: Comments in the middle +brief: > + Comments in the middle +yaml: | + foo: + # some comment + # some comment + bar: foo + # some comment + # some comment +php: | + array('foo' => array('bar' => 'foo')) +--- +test: Comments on a hash line +brief: > + Comments on a hash line +yaml: | + foo: # a comment + foo: bar # a comment +php: | + array('foo' => array('foo' => 'bar')) +--- +test: 'Value starting with a #' +brief: > + 'Value starting with a #' +yaml: | + foo: '#bar' +php: | + array('foo' => '#bar') +--- +test: Document starting with a comment and a separator +brief: > + Commenting before document start is allowed +yaml: | + # document comment + --- + foo: bar # a comment +php: | + array('foo' => 'bar') +--- +test: Comment containing a colon on a hash line +brief: > + Comment containing a colon on a scalar line +yaml: 'foo # comment: this is also part of the comment' +php: | + 'foo' +--- +test: 'Hash key containing a #' +brief: > + 'Hash key containing a #' +yaml: 'foo#bar: baz' +php: | + array('foo#bar' => 'baz') +--- +test: 'Hash key ending with a space and a #' +brief: > + 'Hash key ending with a space and a #' +yaml: | + 'foo #': baz +php: | + array('foo #' => 'baz') diff --git a/vendor/symfony/yaml/Tests/Fixtures/sfCompact.yml b/vendor/symfony/yaml/Tests/Fixtures/sfCompact.yml new file mode 100644 index 0000000..1339d23 --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/sfCompact.yml @@ -0,0 +1,159 @@ +--- %YAML:1.0 +test: Compact notation +brief: | + Compact notation for sets of mappings with single element +yaml: | + --- + # products purchased + - item : Super Hoop + - item : Basketball + quantity: 1 + - item: + name: Big Shoes + nick: Biggies + quantity: 1 +php: | + array ( + array ( + 'item' => 'Super Hoop', + ), + array ( + 'item' => 'Basketball', + 'quantity' => 1, + ), + array ( + 'item' => array( + 'name' => 'Big Shoes', + 'nick' => 'Biggies' + ), + 'quantity' => 1 + ) + ) +--- +test: Compact notation combined with inline notation +brief: | + Combinations of compact and inline notation are allowed +yaml: | + --- + items: + - { item: Super Hoop, quantity: 1 } + - [ Basketball, Big Shoes ] +php: | + array ( + 'items' => array ( + array ( + 'item' => 'Super Hoop', + 'quantity' => 1, + ), + array ( + 'Basketball', + 'Big Shoes' + ) + ) + ) +--- %YAML:1.0 +test: Compact notation +brief: | + Compact notation for sets of mappings with single element +yaml: | + --- + # products purchased + - item : Super Hoop + - item : Basketball + quantity: 1 + - item: + name: Big Shoes + nick: Biggies + quantity: 1 +php: | + array ( + array ( + 'item' => 'Super Hoop', + ), + array ( + 'item' => 'Basketball', + 'quantity' => 1, + ), + array ( + 'item' => array( + 'name' => 'Big Shoes', + 'nick' => 'Biggies' + ), + 'quantity' => 1 + ) + ) +--- +test: Compact notation combined with inline notation +brief: | + Combinations of compact and inline notation are allowed +yaml: | + --- + items: + - { item: Super Hoop, quantity: 1 } + - [ Basketball, Big Shoes ] +php: | + array ( + 'items' => array ( + array ( + 'item' => 'Super Hoop', + 'quantity' => 1, + ), + array ( + 'Basketball', + 'Big Shoes' + ) + ) + ) +--- %YAML:1.0 +test: Compact notation +brief: | + Compact notation for sets of mappings with single element +yaml: | + --- + # products purchased + - item : Super Hoop + - item : Basketball + quantity: 1 + - item: + name: Big Shoes + nick: Biggies + quantity: 1 +php: | + array ( + array ( + 'item' => 'Super Hoop', + ), + array ( + 'item' => 'Basketball', + 'quantity' => 1, + ), + array ( + 'item' => array( + 'name' => 'Big Shoes', + 'nick' => 'Biggies' + ), + 'quantity' => 1 + ) + ) +--- +test: Compact notation combined with inline notation +brief: | + Combinations of compact and inline notation are allowed +yaml: | + --- + items: + - { item: Super Hoop, quantity: 1 } + - [ Basketball, Big Shoes ] +php: | + array ( + 'items' => array ( + array ( + 'item' => 'Super Hoop', + 'quantity' => 1, + ), + array ( + 'Basketball', + 'Big Shoes' + ) + ) + ) diff --git a/vendor/symfony/yaml/Tests/Fixtures/sfMergeKey.yml b/vendor/symfony/yaml/Tests/Fixtures/sfMergeKey.yml new file mode 100644 index 0000000..4b67d34 --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/sfMergeKey.yml @@ -0,0 +1,58 @@ +--- %YAML:1.0 +test: Simple In Place Substitution +brief: > + If you want to reuse an entire alias, only overwriting what is different + you can use a << in place substitution. This is not part of the official + YAML spec, but a widely implemented extension. See the following URL for + details: http://yaml.org/type/merge.html +yaml: | + foo: &foo + a: Steve + b: Clark + c: Brian + bar: + a: before + d: other + <<: *foo + b: new + x: Oren + c: + foo: bar + foo: ignore + bar: foo + duplicate: + foo: bar + foo: ignore + foo2: &foo2 + a: Ballmer + ding: &dong [ fi, fei, fo, fam] + check: + <<: + - *foo + - *dong + isit: tested + head: + <<: [ *foo , *dong , *foo2 ] + taz: &taz + a: Steve + w: + p: 1234 + nested: + <<: *taz + d: Doug + w: &nestedref + p: 12345 + z: + <<: *nestedref +php: | + array( + 'foo' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian'), + 'bar' => array('a' => 'before', 'd' => 'other', 'b' => 'new', 'c' => array('foo' => 'bar', 'bar' => 'foo'), 'x' => 'Oren'), + 'duplicate' => array('foo' => 'bar'), + 'foo2' => array('a' => 'Ballmer'), + 'ding' => array('fi', 'fei', 'fo', 'fam'), + 'check' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian', 'fi', 'fei', 'fo', 'fam', 'isit' => 'tested'), + 'head' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian', 'fi', 'fei', 'fo', 'fam'), + 'taz' => array('a' => 'Steve', 'w' => array('p' => 1234)), + 'nested' => array('a' => 'Steve', 'w' => array('p' => 12345), 'd' => 'Doug', 'z' => array('p' => 12345)) + ) diff --git a/vendor/symfony/yaml/Tests/Fixtures/sfObjects.yml b/vendor/symfony/yaml/Tests/Fixtures/sfObjects.yml new file mode 100644 index 0000000..ee124b2 --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/sfObjects.yml @@ -0,0 +1,11 @@ +--- %YAML:1.0 +test: Objects +brief: > + Comments at the end of a line +yaml: | + ex1: "foo # bar" + ex2: "foo # bar" # comment + ex3: 'foo # bar' # comment + ex4: foo # comment +php: | + array('ex1' => 'foo # bar', 'ex2' => 'foo # bar', 'ex3' => 'foo # bar', 'ex4' => 'foo') diff --git a/vendor/symfony/yaml/Tests/Fixtures/sfQuotes.yml b/vendor/symfony/yaml/Tests/Fixtures/sfQuotes.yml new file mode 100644 index 0000000..7c60bae --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/sfQuotes.yml @@ -0,0 +1,33 @@ +--- %YAML:1.0 +test: Some characters at the beginning of a string must be escaped +brief: > + Some characters at the beginning of a string must be escaped +yaml: | + foo: '| bar' +php: | + array('foo' => '| bar') +--- +test: A key can be a quoted string +brief: > + A key can be a quoted string +yaml: | + "foo1": bar + 'foo2': bar + "foo \" bar": bar + 'foo '' bar': bar + 'foo3: ': bar + "foo4: ": bar + foo5: { "foo \" bar: ": bar, 'foo '' bar: ': bar } +php: | + array( + 'foo1' => 'bar', + 'foo2' => 'bar', + 'foo " bar' => 'bar', + 'foo \' bar' => 'bar', + 'foo3: ' => 'bar', + 'foo4: ' => 'bar', + 'foo5' => array( + 'foo " bar: ' => 'bar', + 'foo \' bar: ' => 'bar', + ), + ) diff --git a/vendor/symfony/yaml/Tests/Fixtures/sfTests.yml b/vendor/symfony/yaml/Tests/Fixtures/sfTests.yml new file mode 100644 index 0000000..a427be1 --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/sfTests.yml @@ -0,0 +1,149 @@ +--- %YAML:1.0 +test: Multiple quoted string on one line +brief: > + Multiple quoted string on one line +yaml: | + stripped_title: { name: "foo bar", help: "bar foo" } +php: | + array('stripped_title' => array('name' => 'foo bar', 'help' => 'bar foo')) +--- +test: Empty sequence +yaml: | + foo: [ ] +php: | + array('foo' => array()) +--- +test: Empty value +yaml: | + foo: +php: | + array('foo' => null) +--- +test: Inline string parsing +brief: > + Inline string parsing +yaml: | + test: ['complex: string', 'another [string]'] +php: | + array('test' => array('complex: string', 'another [string]')) +--- +test: Boolean +brief: > + Boolean +yaml: | + - false + - true + - null + - ~ + - 'false' + - 'true' + - 'null' + - '~' +php: | + array( + false, + true, + null, + null, + 'false', + 'true', + 'null', + '~', + ) +--- +test: Empty lines in literal blocks +brief: > + Empty lines in literal blocks +yaml: | + foo: + bar: | + foo + + + + bar +php: | + array('foo' => array('bar' => "foo\n\n\n \nbar\n")) +--- +test: Empty lines in folded blocks +brief: > + Empty lines in folded blocks +yaml: | + foo: + bar: > + + foo + + + bar +php: | + array('foo' => array('bar' => "\nfoo\n\nbar\n")) +--- +test: IP addresses +brief: > + IP addresses +yaml: | + foo: 10.0.0.2 +php: | + array('foo' => '10.0.0.2') +--- +test: A sequence with an embedded mapping +brief: > + A sequence with an embedded mapping +yaml: | + - foo + - bar: { bar: foo } +php: | + array('foo', array('bar' => array('bar' => 'foo'))) +--- +test: A sequence with an unordered array +brief: > + A sequence with an unordered array +yaml: | + 1: foo + 0: bar +php: | + array(1 => 'foo', 0 => 'bar') +--- +test: Octal +brief: as in spec example 2.19, octal value is converted +yaml: | + foo: 0123 +php: | + array('foo' => 83) +--- +test: Octal strings +brief: Octal notation in a string must remain a string +yaml: | + foo: "0123" +php: | + array('foo' => '0123') +--- +test: Octal strings +brief: Octal notation in a string must remain a string +yaml: | + foo: '0123' +php: | + array('foo' => '0123') +--- +test: Octal strings +brief: Octal notation in a string must remain a string +yaml: | + foo: | + 0123 +php: | + array('foo' => "0123\n") +--- +test: Document as a simple hash +brief: Document as a simple hash +yaml: | + { foo: bar } +php: | + array('foo' => 'bar') +--- +test: Document as a simple array +brief: Document as a simple array +yaml: | + [ foo, bar ] +php: | + array('foo', 'bar') diff --git a/vendor/symfony/yaml/Tests/Fixtures/unindentedCollections.yml b/vendor/symfony/yaml/Tests/Fixtures/unindentedCollections.yml new file mode 100644 index 0000000..0c96108 --- /dev/null +++ b/vendor/symfony/yaml/Tests/Fixtures/unindentedCollections.yml @@ -0,0 +1,82 @@ +--- %YAML:1.0 +test: Unindented collection +brief: > + Unindented collection +yaml: | + collection: + - item1 + - item2 + - item3 +php: | + array('collection' => array('item1', 'item2', 'item3')) +--- +test: Nested unindented collection (two levels) +brief: > + Nested unindented collection +yaml: | + collection: + key: + - a + - b + - c +php: | + array('collection' => array('key' => array('a', 'b', 'c'))) +--- +test: Nested unindented collection (three levels) +brief: > + Nested unindented collection +yaml: | + collection: + key: + subkey: + - one + - two + - three +php: | + array('collection' => array('key' => array('subkey' => array('one', 'two', 'three')))) +--- +test: Key/value after unindented collection (1) +brief: > + Key/value after unindented collection (1) +yaml: | + collection: + key: + - a + - b + - c + foo: bar +php: | + array('collection' => array('key' => array('a', 'b', 'c')), 'foo' => 'bar') +--- +test: Key/value after unindented collection (at the same level) +brief: > + Key/value after unindented collection +yaml: | + collection: + key: + - a + - b + - c + foo: bar +php: | + array('collection' => array('key' => array('a', 'b', 'c'), 'foo' => 'bar')) +--- +test: Shortcut Key after unindented collection +brief: > + Key/value after unindented collection +yaml: | + collection: + - key: foo + foo: bar +php: | + array('collection' => array(array('key' => 'foo', 'foo' => 'bar'))) +--- +test: Shortcut Key after unindented collection with custom spaces +brief: > + Key/value after unindented collection +yaml: | + collection: + - key: foo + foo: bar +php: | + array('collection' => array(array('key' => 'foo', 'foo' => 'bar'))) diff --git a/vendor/symfony/yaml/Tests/InlineTest.php b/vendor/symfony/yaml/Tests/InlineTest.php new file mode 100644 index 0000000..3da5dd5 --- /dev/null +++ b/vendor/symfony/yaml/Tests/InlineTest.php @@ -0,0 +1,429 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml\Tests; + +use Symfony\Component\Yaml\Inline; + +class InlineTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getTestsForParse + */ + public function testParse($yaml, $value) + { + $this->assertSame($value, Inline::parse($yaml), sprintf('::parse() converts an inline YAML to a PHP structure (%s)', $yaml)); + } + + /** + * @dataProvider getTestsForParseWithMapObjects + */ + public function testParseWithMapObjects($yaml, $value) + { + $actual = Inline::parse($yaml, false, false, true); + + $this->assertSame(serialize($value), serialize($actual)); + } + + /** + * @dataProvider getTestsForDump + */ + public function testDump($yaml, $value) + { + $this->assertEquals($yaml, Inline::dump($value), sprintf('::dump() converts a PHP structure to an inline YAML (%s)', $yaml)); + + $this->assertSame($value, Inline::parse(Inline::dump($value)), 'check consistency'); + } + + public function testDumpNumericValueWithLocale() + { + $locale = setlocale(LC_NUMERIC, 0); + if (false === $locale) { + $this->markTestSkipped('Your platform does not support locales.'); + } + + try { + $requiredLocales = array('fr_FR.UTF-8', 'fr_FR.UTF8', 'fr_FR.utf-8', 'fr_FR.utf8', 'French_France.1252'); + if (false === setlocale(LC_NUMERIC, $requiredLocales)) { + $this->markTestSkipped('Could not set any of required locales: '.implode(', ', $requiredLocales)); + } + + $this->assertEquals('1.2', Inline::dump(1.2)); + $this->assertContains('fr', strtolower(setlocale(LC_NUMERIC, 0))); + } finally { + setlocale(LC_NUMERIC, $locale); + } + } + + public function testHashStringsResemblingExponentialNumericsShouldNotBeChangedToINF() + { + $value = '686e444'; + + $this->assertSame($value, Inline::parse(Inline::dump($value))); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + * @expectedExceptionMessage Found unknown escape character "\V". + */ + public function testParseScalarWithNonEscapedBlackslashShouldThrowException() + { + Inline::parse('"Foo\Var"'); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + */ + public function testParseScalarWithNonEscapedBlackslashAtTheEndShouldThrowException() + { + Inline::parse('"Foo\\"'); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + */ + public function testParseScalarWithIncorrectlyQuotedStringShouldThrowException() + { + $value = "'don't do somthin' like that'"; + Inline::parse($value); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + */ + public function testParseScalarWithIncorrectlyDoubleQuotedStringShouldThrowException() + { + $value = '"don"t do somthin" like that"'; + Inline::parse($value); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + */ + public function testParseInvalidMappingKeyShouldThrowException() + { + $value = '{ "foo " bar": "bar" }'; + Inline::parse($value); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + */ + public function testParseInvalidMappingShouldThrowException() + { + Inline::parse('[foo] bar'); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + */ + public function testParseInvalidSequenceShouldThrowException() + { + Inline::parse('{ foo: bar } bar'); + } + + public function testParseScalarWithCorrectlyQuotedStringShouldReturnString() + { + $value = "'don''t do somthin'' like that'"; + $expect = "don't do somthin' like that"; + + $this->assertSame($expect, Inline::parseScalar($value)); + } + + /** + * @dataProvider getDataForParseReferences + */ + public function testParseReferences($yaml, $expected) + { + $this->assertSame($expected, Inline::parse($yaml, false, false, false, array('var' => 'var-value'))); + } + + public function getDataForParseReferences() + { + return array( + 'scalar' => array('*var', 'var-value'), + 'list' => array('[ *var ]', array('var-value')), + 'list-in-list' => array('[[ *var ]]', array(array('var-value'))), + 'map-in-list' => array('[ { key: *var } ]', array(array('key' => 'var-value'))), + 'embedded-mapping-in-list' => array('[ key: *var ]', array(array('key' => 'var-value'))), + 'map' => array('{ key: *var }', array('key' => 'var-value')), + 'list-in-map' => array('{ key: [*var] }', array('key' => array('var-value'))), + 'map-in-map' => array('{ foo: { bar: *var } }', array('foo' => array('bar' => 'var-value'))), + ); + } + + public function testParseMapReferenceInSequence() + { + $foo = array( + 'a' => 'Steve', + 'b' => 'Clark', + 'c' => 'Brian', + ); + $this->assertSame(array($foo), Inline::parse('[*foo]', false, false, false, array('foo' => $foo))); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + * @expectedExceptionMessage A reference must contain at least one character. + */ + public function testParseUnquotedAsterisk() + { + Inline::parse('{ foo: * }'); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + * @expectedExceptionMessage A reference must contain at least one character. + */ + public function testParseUnquotedAsteriskFollowedByAComment() + { + Inline::parse('{ foo: * #foo }'); + } + + /** + * @dataProvider getReservedIndicators + * @expectedException Symfony\Component\Yaml\Exception\ParseException + * @expectedExceptionMessage cannot start a plain scalar; you need to quote the scalar. + */ + public function testParseUnquotedScalarStartingWithReservedIndicator($indicator) + { + Inline::parse(sprintf('{ foo: %sfoo }', $indicator)); + } + + public function getReservedIndicators() + { + return array(array('@'), array('`')); + } + + /** + * @dataProvider getScalarIndicators + * @expectedException Symfony\Component\Yaml\Exception\ParseException + * @expectedExceptionMessage cannot start a plain scalar; you need to quote the scalar. + */ + public function testParseUnquotedScalarStartingWithScalarIndicator($indicator) + { + Inline::parse(sprintf('{ foo: %sfoo }', $indicator)); + } + + public function getScalarIndicators() + { + return array(array('|'), array('>')); + } + + public function getTestsForParse() + { + return array( + array('', ''), + array('null', null), + array('false', false), + array('true', true), + array('12', 12), + array('-12', -12), + array('"quoted string"', 'quoted string'), + array("'quoted string'", 'quoted string'), + array('12.30e+02', 12.30e+02), + array('0x4D2', 0x4D2), + array('02333', 02333), + array('.Inf', -log(0)), + array('-.Inf', log(0)), + array("'686e444'", '686e444'), + array('686e444', 646e444), + array('123456789123456789123456789123456789', '123456789123456789123456789123456789'), + array('"foo\r\nbar"', "foo\r\nbar"), + array("'foo#bar'", 'foo#bar'), + array("'foo # bar'", 'foo # bar'), + array("'#cfcfcf'", '#cfcfcf'), + array('::form_base.html.twig', '::form_base.html.twig'), + + // Pre-YAML-1.2 booleans + array("'y'", 'y'), + array("'n'", 'n'), + array("'yes'", 'yes'), + array("'no'", 'no'), + array("'on'", 'on'), + array("'off'", 'off'), + + array('2007-10-30', gmmktime(0, 0, 0, 10, 30, 2007)), + array('2007-10-30T02:59:43Z', gmmktime(2, 59, 43, 10, 30, 2007)), + array('2007-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 2007)), + array('1960-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 1960)), + array('1730-10-30T02:59:43Z', gmmktime(2, 59, 43, 10, 30, 1730)), + + array('"a \\"string\\" with \'quoted strings inside\'"', 'a "string" with \'quoted strings inside\''), + array("'a \"string\" with ''quoted strings inside'''", 'a "string" with \'quoted strings inside\''), + + // sequences + // urls are no key value mapping. see #3609. Valid yaml "key: value" mappings require a space after the colon + array('[foo, http://urls.are/no/mappings, false, null, 12]', array('foo', 'http://urls.are/no/mappings', false, null, 12)), + array('[ foo , bar , false , null , 12 ]', array('foo', 'bar', false, null, 12)), + array('[\'foo,bar\', \'foo bar\']', array('foo,bar', 'foo bar')), + + // mappings + array('{foo:bar,bar:foo,false:false,null:null,integer:12}', array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)), + array('{ foo : bar, bar : foo, false : false, null : null, integer : 12 }', array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)), + array('{foo: \'bar\', bar: \'foo: bar\'}', array('foo' => 'bar', 'bar' => 'foo: bar')), + array('{\'foo\': \'bar\', "bar": \'foo: bar\'}', array('foo' => 'bar', 'bar' => 'foo: bar')), + array('{\'foo\'\'\': \'bar\', "bar\"": \'foo: bar\'}', array('foo\'' => 'bar', 'bar"' => 'foo: bar')), + array('{\'foo: \': \'bar\', "bar: ": \'foo: bar\'}', array('foo: ' => 'bar', 'bar: ' => 'foo: bar')), + + // nested sequences and mappings + array('[foo, [bar, foo]]', array('foo', array('bar', 'foo'))), + array('[foo, {bar: foo}]', array('foo', array('bar' => 'foo'))), + array('{ foo: {bar: foo} }', array('foo' => array('bar' => 'foo'))), + array('{ foo: [bar, foo] }', array('foo' => array('bar', 'foo'))), + + array('[ foo, [ bar, foo ] ]', array('foo', array('bar', 'foo'))), + + array('[{ foo: {bar: foo} }]', array(array('foo' => array('bar' => 'foo')))), + + array('[foo, [bar, [foo, [bar, foo]], foo]]', array('foo', array('bar', array('foo', array('bar', 'foo')), 'foo'))), + + array('[foo, {bar: foo, foo: [foo, {bar: foo}]}, [foo, {bar: foo}]]', array('foo', array('bar' => 'foo', 'foo' => array('foo', array('bar' => 'foo'))), array('foo', array('bar' => 'foo')))), + + array('[foo, bar: { foo: bar }]', array('foo', '1' => array('bar' => array('foo' => 'bar')))), + array('[foo, \'@foo.baz\', { \'%foo%\': \'foo is %foo%\', bar: \'%foo%\' }, true, \'@service_container\']', array('foo', '@foo.baz', array('%foo%' => 'foo is %foo%', 'bar' => '%foo%'), true, '@service_container')), + ); + } + + public function getTestsForParseWithMapObjects() + { + return array( + array('', ''), + array('null', null), + array('false', false), + array('true', true), + array('12', 12), + array('-12', -12), + array('"quoted string"', 'quoted string'), + array("'quoted string'", 'quoted string'), + array('12.30e+02', 12.30e+02), + array('0x4D2', 0x4D2), + array('02333', 02333), + array('.Inf', -log(0)), + array('-.Inf', log(0)), + array("'686e444'", '686e444'), + array('686e444', 646e444), + array('123456789123456789123456789123456789', '123456789123456789123456789123456789'), + array('"foo\r\nbar"', "foo\r\nbar"), + array("'foo#bar'", 'foo#bar'), + array("'foo # bar'", 'foo # bar'), + array("'#cfcfcf'", '#cfcfcf'), + array('::form_base.html.twig', '::form_base.html.twig'), + + array('2007-10-30', gmmktime(0, 0, 0, 10, 30, 2007)), + array('2007-10-30T02:59:43Z', gmmktime(2, 59, 43, 10, 30, 2007)), + array('2007-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 2007)), + array('1960-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 1960)), + array('1730-10-30T02:59:43Z', gmmktime(2, 59, 43, 10, 30, 1730)), + + array('"a \\"string\\" with \'quoted strings inside\'"', 'a "string" with \'quoted strings inside\''), + array("'a \"string\" with ''quoted strings inside'''", 'a "string" with \'quoted strings inside\''), + + // sequences + // urls are no key value mapping. see #3609. Valid yaml "key: value" mappings require a space after the colon + array('[foo, http://urls.are/no/mappings, false, null, 12]', array('foo', 'http://urls.are/no/mappings', false, null, 12)), + array('[ foo , bar , false , null , 12 ]', array('foo', 'bar', false, null, 12)), + array('[\'foo,bar\', \'foo bar\']', array('foo,bar', 'foo bar')), + + // mappings + array('{foo:bar,bar:foo,false:false,null:null,integer:12}', (object) array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)), + array('{ foo : bar, bar : foo, false : false, null : null, integer : 12 }', (object) array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)), + array('{foo: \'bar\', bar: \'foo: bar\'}', (object) array('foo' => 'bar', 'bar' => 'foo: bar')), + array('{\'foo\': \'bar\', "bar": \'foo: bar\'}', (object) array('foo' => 'bar', 'bar' => 'foo: bar')), + array('{\'foo\'\'\': \'bar\', "bar\"": \'foo: bar\'}', (object) array('foo\'' => 'bar', 'bar"' => 'foo: bar')), + array('{\'foo: \': \'bar\', "bar: ": \'foo: bar\'}', (object) array('foo: ' => 'bar', 'bar: ' => 'foo: bar')), + + // nested sequences and mappings + array('[foo, [bar, foo]]', array('foo', array('bar', 'foo'))), + array('[foo, {bar: foo}]', array('foo', (object) array('bar' => 'foo'))), + array('{ foo: {bar: foo} }', (object) array('foo' => (object) array('bar' => 'foo'))), + array('{ foo: [bar, foo] }', (object) array('foo' => array('bar', 'foo'))), + + array('[ foo, [ bar, foo ] ]', array('foo', array('bar', 'foo'))), + + array('[{ foo: {bar: foo} }]', array((object) array('foo' => (object) array('bar' => 'foo')))), + + array('[foo, [bar, [foo, [bar, foo]], foo]]', array('foo', array('bar', array('foo', array('bar', 'foo')), 'foo'))), + + array('[foo, {bar: foo, foo: [foo, {bar: foo}]}, [foo, {bar: foo}]]', array('foo', (object) array('bar' => 'foo', 'foo' => array('foo', (object) array('bar' => 'foo'))), array('foo', (object) array('bar' => 'foo')))), + + array('[foo, bar: { foo: bar }]', array('foo', '1' => (object) array('bar' => (object) array('foo' => 'bar')))), + array('[foo, \'@foo.baz\', { \'%foo%\': \'foo is %foo%\', bar: \'%foo%\' }, true, \'@service_container\']', array('foo', '@foo.baz', (object) array('%foo%' => 'foo is %foo%', 'bar' => '%foo%'), true, '@service_container')), + + array('{}', new \stdClass()), + array('{ foo : bar, bar : {} }', (object) array('foo' => 'bar', 'bar' => new \stdClass())), + array('{ foo : [], bar : {} }', (object) array('foo' => array(), 'bar' => new \stdClass())), + array('{foo: \'bar\', bar: {} }', (object) array('foo' => 'bar', 'bar' => new \stdClass())), + array('{\'foo\': \'bar\', "bar": {}}', (object) array('foo' => 'bar', 'bar' => new \stdClass())), + array('{\'foo\': \'bar\', "bar": \'{}\'}', (object) array('foo' => 'bar', 'bar' => '{}')), + + array('[foo, [{}, {}]]', array('foo', array(new \stdClass(), new \stdClass()))), + array('[foo, [[], {}]]', array('foo', array(array(), new \stdClass()))), + array('[foo, [[{}, {}], {}]]', array('foo', array(array(new \stdClass(), new \stdClass()), new \stdClass()))), + array('[foo, {bar: {}}]', array('foo', '1' => (object) array('bar' => new \stdClass()))), + ); + } + + public function getTestsForDump() + { + return array( + array('null', null), + array('false', false), + array('true', true), + array('12', 12), + array("'quoted string'", 'quoted string'), + array('!!float 1230', 12.30e+02), + array('1234', 0x4D2), + array('1243', 02333), + array('.Inf', -log(0)), + array('-.Inf', log(0)), + array("'686e444'", '686e444'), + array('"foo\r\nbar"', "foo\r\nbar"), + array("'foo#bar'", 'foo#bar'), + array("'foo # bar'", 'foo # bar'), + array("'#cfcfcf'", '#cfcfcf'), + + array("'a \"string\" with ''quoted strings inside'''", 'a "string" with \'quoted strings inside\''), + + array("'-dash'", '-dash'), + array("'-'", '-'), + + // Pre-YAML-1.2 booleans + array("'y'", 'y'), + array("'n'", 'n'), + array("'yes'", 'yes'), + array("'no'", 'no'), + array("'on'", 'on'), + array("'off'", 'off'), + + // sequences + array('[foo, bar, false, null, 12]', array('foo', 'bar', false, null, 12)), + array('[\'foo,bar\', \'foo bar\']', array('foo,bar', 'foo bar')), + + // mappings + array('{ foo: bar, bar: foo, \'false\': false, \'null\': null, integer: 12 }', array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)), + array('{ foo: bar, bar: \'foo: bar\' }', array('foo' => 'bar', 'bar' => 'foo: bar')), + + // nested sequences and mappings + array('[foo, [bar, foo]]', array('foo', array('bar', 'foo'))), + + array('[foo, [bar, [foo, [bar, foo]], foo]]', array('foo', array('bar', array('foo', array('bar', 'foo')), 'foo'))), + + array('{ foo: { bar: foo } }', array('foo' => array('bar' => 'foo'))), + + array('[foo, { bar: foo }]', array('foo', array('bar' => 'foo'))), + + array('[foo, { bar: foo, foo: [foo, { bar: foo }] }, [foo, { bar: foo }]]', array('foo', array('bar' => 'foo', 'foo' => array('foo', array('bar' => 'foo'))), array('foo', array('bar' => 'foo')))), + + array('[foo, \'@foo.baz\', { \'%foo%\': \'foo is %foo%\', bar: \'%foo%\' }, true, \'@service_container\']', array('foo', '@foo.baz', array('%foo%' => 'foo is %foo%', 'bar' => '%foo%'), true, '@service_container')), + ); + } +} diff --git a/vendor/symfony/yaml/Tests/ParseExceptionTest.php b/vendor/symfony/yaml/Tests/ParseExceptionTest.php new file mode 100644 index 0000000..7286d45 --- /dev/null +++ b/vendor/symfony/yaml/Tests/ParseExceptionTest.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml\Tests; + +use Symfony\Component\Yaml\Exception\ParseException; + +class ParseExceptionTest extends \PHPUnit_Framework_TestCase +{ + public function testGetMessage() + { + $exception = new ParseException('Error message', 42, 'foo: bar', '/var/www/app/config.yml'); + $message = 'Error message in "/var/www/app/config.yml" at line 42 (near "foo: bar")'; + + $this->assertEquals($message, $exception->getMessage()); + } + + public function testGetMessageWithUnicodeInFilename() + { + $exception = new ParseException('Error message', 42, 'foo: bar', 'äöü.yml'); + $message = 'Error message in "äöü.yml" at line 42 (near "foo: bar")'; + + $this->assertEquals($message, $exception->getMessage()); + } +} diff --git a/vendor/symfony/yaml/Tests/ParserTest.php b/vendor/symfony/yaml/Tests/ParserTest.php new file mode 100644 index 0000000..fc15c32 --- /dev/null +++ b/vendor/symfony/yaml/Tests/ParserTest.php @@ -0,0 +1,1091 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml\Tests; + +use Symfony\Component\Yaml\Yaml; +use Symfony\Component\Yaml\Parser; + +class ParserTest extends \PHPUnit_Framework_TestCase +{ + protected $parser; + + protected function setUp() + { + $this->parser = new Parser(); + } + + protected function tearDown() + { + $this->parser = null; + } + + /** + * @dataProvider getDataFormSpecifications + */ + public function testSpecifications($file, $expected, $yaml, $comment) + { + $this->assertEquals($expected, var_export($this->parser->parse($yaml), true), $comment); + } + + public function getDataFormSpecifications() + { + $parser = new Parser(); + $path = __DIR__.'/Fixtures'; + + $tests = array(); + $files = $parser->parse(file_get_contents($path.'/index.yml')); + foreach ($files as $file) { + $yamls = file_get_contents($path.'/'.$file.'.yml'); + + // split YAMLs documents + foreach (preg_split('/^---( %YAML\:1\.0)?/m', $yamls) as $yaml) { + if (!$yaml) { + continue; + } + + $test = $parser->parse($yaml); + if (isset($test['todo']) && $test['todo']) { + // TODO + } else { + eval('$expected = '.trim($test['php']).';'); + + $tests[] = array($file, var_export($expected, true), $test['yaml'], $test['test']); + } + } + } + + return $tests; + } + + public function testTabsInYaml() + { + // test tabs in YAML + $yamls = array( + "foo:\n bar", + "foo:\n bar", + "foo:\n bar", + "foo:\n bar", + ); + + foreach ($yamls as $yaml) { + try { + $content = $this->parser->parse($yaml); + + $this->fail('YAML files must not contain tabs'); + } catch (\Exception $e) { + $this->assertInstanceOf('\Exception', $e, 'YAML files must not contain tabs'); + $this->assertEquals('A YAML file cannot contain tabs as indentation at line 2 (near "'.strpbrk($yaml, "\t").'").', $e->getMessage(), 'YAML files must not contain tabs'); + } + } + } + + public function testEndOfTheDocumentMarker() + { + $yaml = <<<'EOF' +--- %YAML:1.0 +foo +... +EOF; + + $this->assertEquals('foo', $this->parser->parse($yaml)); + } + + public function getBlockChompingTests() + { + $tests = array(); + + $yaml = <<<'EOF' +foo: |- + one + two +bar: |- + one + two + +EOF; + $expected = array( + 'foo' => "one\ntwo", + 'bar' => "one\ntwo", + ); + $tests['Literal block chomping strip with single trailing newline'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: |- + one + two + +bar: |- + one + two + + +EOF; + $expected = array( + 'foo' => "one\ntwo", + 'bar' => "one\ntwo", + ); + $tests['Literal block chomping strip with multiple trailing newlines'] = array($expected, $yaml); + + $yaml = <<<'EOF' +{} + + +EOF; + $expected = array(); + $tests['Literal block chomping strip with multiple trailing newlines after a 1-liner'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: |- + one + two +bar: |- + one + two +EOF; + $expected = array( + 'foo' => "one\ntwo", + 'bar' => "one\ntwo", + ); + $tests['Literal block chomping strip without trailing newline'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: | + one + two +bar: | + one + two + +EOF; + $expected = array( + 'foo' => "one\ntwo\n", + 'bar' => "one\ntwo\n", + ); + $tests['Literal block chomping clip with single trailing newline'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: | + one + two + +bar: | + one + two + + +EOF; + $expected = array( + 'foo' => "one\ntwo\n", + 'bar' => "one\ntwo\n", + ); + $tests['Literal block chomping clip with multiple trailing newlines'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: | + one + two +bar: | + one + two +EOF; + $expected = array( + 'foo' => "one\ntwo\n", + 'bar' => "one\ntwo", + ); + $tests['Literal block chomping clip without trailing newline'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: |+ + one + two +bar: |+ + one + two + +EOF; + $expected = array( + 'foo' => "one\ntwo\n", + 'bar' => "one\ntwo\n", + ); + $tests['Literal block chomping keep with single trailing newline'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: |+ + one + two + +bar: |+ + one + two + + +EOF; + $expected = array( + 'foo' => "one\ntwo\n\n", + 'bar' => "one\ntwo\n\n", + ); + $tests['Literal block chomping keep with multiple trailing newlines'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: |+ + one + two +bar: |+ + one + two +EOF; + $expected = array( + 'foo' => "one\ntwo\n", + 'bar' => "one\ntwo", + ); + $tests['Literal block chomping keep without trailing newline'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: >- + one + two +bar: >- + one + two + +EOF; + $expected = array( + 'foo' => 'one two', + 'bar' => 'one two', + ); + $tests['Folded block chomping strip with single trailing newline'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: >- + one + two + +bar: >- + one + two + + +EOF; + $expected = array( + 'foo' => 'one two', + 'bar' => 'one two', + ); + $tests['Folded block chomping strip with multiple trailing newlines'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: >- + one + two +bar: >- + one + two +EOF; + $expected = array( + 'foo' => 'one two', + 'bar' => 'one two', + ); + $tests['Folded block chomping strip without trailing newline'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: > + one + two +bar: > + one + two + +EOF; + $expected = array( + 'foo' => "one two\n", + 'bar' => "one two\n", + ); + $tests['Folded block chomping clip with single trailing newline'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: > + one + two + +bar: > + one + two + + +EOF; + $expected = array( + 'foo' => "one two\n", + 'bar' => "one two\n", + ); + $tests['Folded block chomping clip with multiple trailing newlines'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: > + one + two +bar: > + one + two +EOF; + $expected = array( + 'foo' => "one two\n", + 'bar' => 'one two', + ); + $tests['Folded block chomping clip without trailing newline'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: >+ + one + two +bar: >+ + one + two + +EOF; + $expected = array( + 'foo' => "one two\n", + 'bar' => "one two\n", + ); + $tests['Folded block chomping keep with single trailing newline'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: >+ + one + two + +bar: >+ + one + two + + +EOF; + $expected = array( + 'foo' => "one two\n\n", + 'bar' => "one two\n\n", + ); + $tests['Folded block chomping keep with multiple trailing newlines'] = array($expected, $yaml); + + $yaml = <<<'EOF' +foo: >+ + one + two +bar: >+ + one + two +EOF; + $expected = array( + 'foo' => "one two\n", + 'bar' => 'one two', + ); + $tests['Folded block chomping keep without trailing newline'] = array($expected, $yaml); + + return $tests; + } + + /** + * @dataProvider getBlockChompingTests + */ + public function testBlockChomping($expected, $yaml) + { + $this->assertSame($expected, $this->parser->parse($yaml)); + } + + /** + * Regression test for issue #7989. + * + * @see https://github.com/symfony/symfony/issues/7989 + */ + public function testBlockLiteralWithLeadingNewlines() + { + $yaml = <<<'EOF' +foo: |- + + + bar + +EOF; + $expected = array( + 'foo' => "\n\nbar", + ); + + $this->assertSame($expected, $this->parser->parse($yaml)); + } + + public function testObjectSupportEnabled() + { + $input = <<assertEquals(array('foo' => new B(), 'bar' => 1), $this->parser->parse($input, false, true), '->parse() is able to parse objects'); + + $input = <<assertEquals(array('foo' => new B(), 'bar' => 1), $this->parser->parse($input, false, true), '->parse() is able to parse objects'); + } + + /** + * @dataProvider invalidDumpedObjectProvider + */ + public function testObjectSupportDisabledButNoExceptions($input) + { + $this->assertEquals(array('foo' => null, 'bar' => 1), $this->parser->parse($input), '->parse() does not parse objects'); + } + + /** + * @dataProvider getObjectForMapTests + */ + public function testObjectForMap($yaml, $expected) + { + $this->assertEquals($expected, $this->parser->parse($yaml, false, false, true)); + } + + public function getObjectForMapTests() + { + $tests = array(); + + $yaml = <<foo = new \stdClass(); + $expected->foo->fiz = array('cat'); + $tests['mapping'] = array($yaml, $expected); + + $yaml = '{ "foo": "bar", "fiz": "cat" }'; + $expected = new \stdClass(); + $expected->foo = 'bar'; + $expected->fiz = 'cat'; + $tests['inline-mapping'] = array($yaml, $expected); + + $yaml = "foo: bar\nbaz: foobar"; + $expected = new \stdClass(); + $expected->foo = 'bar'; + $expected->baz = 'foobar'; + $tests['object-for-map-is-applied-after-parsing'] = array($yaml, $expected); + + $yaml = <<array = array(); + $expected->array[0] = new \stdClass(); + $expected->array[0]->key = 'one'; + $expected->array[1] = new \stdClass(); + $expected->array[1]->key = 'two'; + $tests['nest-map-and-sequence'] = array($yaml, $expected); + + $yaml = <<map = new \stdClass(); + $expected->map->{1} = 'one'; + $expected->map->{2} = 'two'; + $tests['numeric-keys'] = array($yaml, $expected); + + $yaml = <<map = new \stdClass(); + $expected->map->{0} = 'one'; + $expected->map->{1} = 'two'; + $tests['zero-indexed-numeric-keys'] = array($yaml, $expected); + + return $tests; + } + + /** + * @dataProvider invalidDumpedObjectProvider + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + */ + public function testObjectsSupportDisabledWithExceptions($yaml) + { + $this->parser->parse($yaml, true, false); + } + + public function invalidDumpedObjectProvider() + { + $yamlTag = << array($yamlTag), + 'local-tag' => array($localTag), + ); + } + + /** + * @requires extension iconv + */ + public function testNonUtf8Exception() + { + $yamls = array( + iconv('UTF-8', 'ISO-8859-1', "foo: 'äöüß'"), + iconv('UTF-8', 'ISO-8859-15', "euro: '€'"), + iconv('UTF-8', 'CP1252', "cp1252: '©ÉÇáñ'"), + ); + + foreach ($yamls as $yaml) { + try { + $this->parser->parse($yaml); + + $this->fail('charsets other than UTF-8 are rejected.'); + } catch (\Exception $e) { + $this->assertInstanceOf('Symfony\Component\Yaml\Exception\ParseException', $e, 'charsets other than UTF-8 are rejected.'); + } + } + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + */ + public function testUnindentedCollectionException() + { + $yaml = <<<'EOF' + +collection: +-item1 +-item2 +-item3 + +EOF; + + $this->parser->parse($yaml); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + */ + public function testShortcutKeyUnindentedCollectionException() + { + $yaml = <<<'EOF' + +collection: +- key: foo + foo: bar + +EOF; + + $this->parser->parse($yaml); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + * @expectedExceptionMessage Multiple documents are not supported. + */ + public function testMultipleDocumentsNotSupportedException() + { + Yaml::parse(<<<'EOL' +# Ranking of 1998 home runs +--- +- Mark McGwire +- Sammy Sosa +- Ken Griffey + +# Team ranking +--- +- Chicago Cubs +- St Louis Cardinals +EOL + ); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + */ + public function testSequenceInAMapping() + { + Yaml::parse(<<<'EOF' +yaml: + hash: me + - array stuff +EOF + ); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + */ + public function testMappingInASequence() + { + Yaml::parse(<<<'EOF' +yaml: + - array stuff + hash: me +EOF + ); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + * @expectedExceptionMessage missing colon + */ + public function testScalarInSequence() + { + Yaml::parse(<< It is an error for two equal keys to appear in the same mapping node. + * > In such a case the YAML processor may continue, ignoring the second + * > `key: value` pair and issuing an appropriate warning. This strategy + * > preserves a consistent information model for one-pass and random access + * > applications. + * + * @see http://yaml.org/spec/1.2/spec.html#id2759572 + * @see http://yaml.org/spec/1.1/#id932806 + */ + public function testMappingDuplicateKeyBlock() + { + $input = << array( + 'child' => 'first', + ), + ); + $this->assertSame($expected, Yaml::parse($input)); + } + + public function testMappingDuplicateKeyFlow() + { + $input = << array( + 'child' => 'first', + ), + ); + $this->assertSame($expected, Yaml::parse($input)); + } + + public function testEmptyValue() + { + $input = <<<'EOF' +hash: +EOF; + + $this->assertEquals(array('hash' => null), Yaml::parse($input)); + } + + public function testCommentAtTheRootIndent() + { + $this->assertEquals(array( + 'services' => array( + 'app.foo_service' => array( + 'class' => 'Foo', + ), + 'app/bar_service' => array( + 'class' => 'Bar', + ), + ), + ), Yaml::parse(<<<'EOF' +# comment 1 +services: +# comment 2 + # comment 3 + app.foo_service: + class: Foo +# comment 4 + # comment 5 + app/bar_service: + class: Bar +EOF + )); + } + + public function testStringBlockWithComments() + { + $this->assertEquals(array('content' => <<<'EOT' +# comment 1 +header + + # comment 2 + +

title

+ + +footer # comment3 +EOT + ), Yaml::parse(<<<'EOF' +content: | + # comment 1 + header + + # comment 2 + +

title

+ + + footer # comment3 +EOF + )); + } + + public function testFoldedStringBlockWithComments() + { + $this->assertEquals(array(array('content' => <<<'EOT' +# comment 1 +header + + # comment 2 + +

title

+ + +footer # comment3 +EOT + )), Yaml::parse(<<<'EOF' +- + content: | + # comment 1 + header + + # comment 2 + +

title

+ + + footer # comment3 +EOF + )); + } + + public function testNestedFoldedStringBlockWithComments() + { + $this->assertEquals(array(array( + 'title' => 'some title', + 'content' => <<<'EOT' +# comment 1 +header + + # comment 2 + +

title

+ + +footer # comment3 +EOT + )), Yaml::parse(<<<'EOF' +- + title: some title + content: | + # comment 1 + header + + # comment 2 + +

title

+ + + footer # comment3 +EOF + )); + } + + public function testReferenceResolvingInInlineStrings() + { + $this->assertEquals(array( + 'var' => 'var-value', + 'scalar' => 'var-value', + 'list' => array('var-value'), + 'list_in_list' => array(array('var-value')), + 'map_in_list' => array(array('key' => 'var-value')), + 'embedded_mapping' => array(array('key' => 'var-value')), + 'map' => array('key' => 'var-value'), + 'list_in_map' => array('key' => array('var-value')), + 'map_in_map' => array('foo' => array('bar' => 'var-value')), + ), Yaml::parse(<<<'EOF' +var: &var var-value +scalar: *var +list: [ *var ] +list_in_list: [[ *var ]] +map_in_list: [ { key: *var } ] +embedded_mapping: [ key: *var ] +map: { key: *var } +list_in_map: { key: [*var] } +map_in_map: { foo: { bar: *var } } +EOF + )); + } + + public function testYamlDirective() + { + $yaml = <<<'EOF' +%YAML 1.2 +--- +foo: 1 +bar: 2 +EOF; + $this->assertEquals(array('foo' => 1, 'bar' => 2), $this->parser->parse($yaml)); + } + + public function testFloatKeys() + { + $yaml = <<<'EOF' +foo: + 1.2: "bar" + 1.3: "baz" +EOF; + + $expected = array( + 'foo' => array( + '1.2' => 'bar', + '1.3' => 'baz', + ), + ); + + $this->assertEquals($expected, $this->parser->parse($yaml)); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + * @expectedExceptionMessage A colon cannot be used in an unquoted mapping value + */ + public function testColonInMappingValueException() + { + $yaml = <<parser->parse($yaml); + } + + public function testColonInMappingValueExceptionNotTriggeredByColonInComment() + { + $yaml = <<assertSame(array('foo' => array('bar' => 'foobar')), $this->parser->parse($yaml)); + } + + /** + * @dataProvider getCommentLikeStringInScalarBlockData + */ + public function testCommentLikeStringsAreNotStrippedInBlockScalars($yaml, $expectedParserResult) + { + $this->assertSame($expectedParserResult, $this->parser->parse($yaml)); + } + + public function getCommentLikeStringInScalarBlockData() + { + $tests = array(); + + $yaml = <<<'EOT' +pages: + - + title: some title + content: | + # comment 1 + header + + # comment 2 + +

title

+ + + footer # comment3 +EOT; + $expected = array( + 'pages' => array( + array( + 'title' => 'some title', + 'content' => <<<'EOT' +# comment 1 +header + + # comment 2 + +

title

+ + +footer # comment3 +EOT + , + ), + ), + ); + $tests[] = array($yaml, $expected); + + $yaml = <<<'EOT' +test: | + foo + # bar + baz +collection: + - one: | + foo + # bar + baz + - two: | + foo + # bar + baz +EOT; + $expected = array( + 'test' => <<<'EOT' +foo +# bar +baz + +EOT + , + 'collection' => array( + array( + 'one' => <<<'EOT' +foo +# bar +baz +EOT + , + ), + array( + 'two' => <<<'EOT' +foo +# bar +baz +EOT + , + ), + ), + ); + $tests[] = array($yaml, $expected); + + $yaml = << + line1 + line2> + baz: +# comment + foobar: ~ +EOT; + $expected = array( + 'foo' => array( + 'bar' => array( + 'scalar-block' => 'line1 line2>', + ), + 'baz' => array( + 'foobar' => null, + ), + ), + ); + $tests[] = array($yaml, $expected); + + $yaml = <<<'EOT' +a: + b: hello +# c: | +# first row +# second row + d: hello +EOT; + $expected = array( + 'a' => array( + 'b' => 'hello', + 'd' => 'hello', + ), + ); + $tests[] = array($yaml, $expected); + + return $tests; + } + + public function testBlankLinesAreParsedAsNewLinesInFoldedBlocks() + { + $yaml = << +

A heading

+ +
    +
  • a list
  • +
  • may be a good example
  • +
+EOT; + + $this->assertSame( + array( + 'test' => <<A heading +
  • a list
  • may be a good example
+EOT + , + ), + $this->parser->parse($yaml) + ); + } + + public function testAdditionallyIndentedLinesAreParsedAsNewLinesInFoldedBlocks() + { + $yaml = << +

A heading

+ +
    +
  • a list
  • +
  • may be a good example
  • +
+EOT; + + $this->assertSame( + array( + 'test' => <<A heading +
    +
  • a list
  • +
  • may be a good example
  • +
+EOT + , + ), + $this->parser->parse($yaml) + ); + } +} + +class B +{ + public $b = 'foo'; +} diff --git a/vendor/symfony/yaml/Tests/YamlTest.php b/vendor/symfony/yaml/Tests/YamlTest.php new file mode 100644 index 0000000..b1a606a --- /dev/null +++ b/vendor/symfony/yaml/Tests/YamlTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml\Tests; + +use Symfony\Component\Yaml\Yaml; + +class YamlTest extends \PHPUnit_Framework_TestCase +{ + public function testParseAndDump() + { + $data = array('lorem' => 'ipsum', 'dolor' => 'sit'); + $yml = Yaml::dump($data); + $parsed = Yaml::parse($yml); + $this->assertEquals($data, $parsed); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The indentation must be greater than zero + */ + public function testZeroIndentationThrowsException() + { + Yaml::dump(array('lorem' => 'ipsum', 'dolor' => 'sit'), 2, 0); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The indentation must be greater than zero + */ + public function testNegativeIndentationThrowsException() + { + Yaml::dump(array('lorem' => 'ipsum', 'dolor' => 'sit'), 2, -4); + } +} diff --git a/vendor/symfony/yaml/Unescaper.php b/vendor/symfony/yaml/Unescaper.php new file mode 100644 index 0000000..6e4f1d7 --- /dev/null +++ b/vendor/symfony/yaml/Unescaper.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml; + +use Symfony\Component\Yaml\Exception\ParseException; + +/** + * Unescaper encapsulates unescaping rules for single and double-quoted + * YAML strings. + * + * @author Matthew Lewinski + * + * @internal + */ +class Unescaper +{ + /** + * Regex fragment that matches an escaped character in a double quoted string. + */ + const REGEX_ESCAPED_CHARACTER = '\\\\(x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|.)'; + + /** + * Unescapes a single quoted string. + * + * @param string $value A single quoted string. + * + * @return string The unescaped string. + */ + public function unescapeSingleQuotedString($value) + { + return str_replace('\'\'', '\'', $value); + } + + /** + * Unescapes a double quoted string. + * + * @param string $value A double quoted string. + * + * @return string The unescaped string. + */ + public function unescapeDoubleQuotedString($value) + { + $callback = function ($match) { + return $this->unescapeCharacter($match[0]); + }; + + // evaluate the string + return preg_replace_callback('/'.self::REGEX_ESCAPED_CHARACTER.'/u', $callback, $value); + } + + /** + * Unescapes a character that was found in a double-quoted string. + * + * @param string $value An escaped character + * + * @return string The unescaped character + */ + private function unescapeCharacter($value) + { + switch ($value[1]) { + case '0': + return "\x0"; + case 'a': + return "\x7"; + case 'b': + return "\x8"; + case 't': + return "\t"; + case "\t": + return "\t"; + case 'n': + return "\n"; + case 'v': + return "\xB"; + case 'f': + return "\xC"; + case 'r': + return "\r"; + case 'e': + return "\x1B"; + case ' ': + return ' '; + case '"': + return '"'; + case '/': + return '/'; + case '\\': + return '\\'; + case 'N': + // U+0085 NEXT LINE + return "\xC2\x85"; + case '_': + // U+00A0 NO-BREAK SPACE + return "\xC2\xA0"; + case 'L': + // U+2028 LINE SEPARATOR + return "\xE2\x80\xA8"; + case 'P': + // U+2029 PARAGRAPH SEPARATOR + return "\xE2\x80\xA9"; + case 'x': + return self::utf8chr(hexdec(substr($value, 2, 2))); + case 'u': + return self::utf8chr(hexdec(substr($value, 2, 4))); + case 'U': + return self::utf8chr(hexdec(substr($value, 2, 8))); + default: + throw new ParseException(sprintf('Found unknown escape character "%s".', $value)); + } + } + + /** + * Get the UTF-8 character for the given code point. + * + * @param int $c The unicode code point + * + * @return string The corresponding UTF-8 character + */ + private static function utf8chr($c) + { + if (0x80 > $c %= 0x200000) { + return chr($c); + } + if (0x800 > $c) { + return chr(0xC0 | $c >> 6).chr(0x80 | $c & 0x3F); + } + if (0x10000 > $c) { + return chr(0xE0 | $c >> 12).chr(0x80 | $c >> 6 & 0x3F).chr(0x80 | $c & 0x3F); + } + + return chr(0xF0 | $c >> 18).chr(0x80 | $c >> 12 & 0x3F).chr(0x80 | $c >> 6 & 0x3F).chr(0x80 | $c & 0x3F); + } +} diff --git a/vendor/symfony/yaml/Yaml.php b/vendor/symfony/yaml/Yaml.php new file mode 100644 index 0000000..caede55 --- /dev/null +++ b/vendor/symfony/yaml/Yaml.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml; + +use Symfony\Component\Yaml\Exception\ParseException; + +/** + * Yaml offers convenience methods to load and dump YAML. + * + * @author Fabien Potencier + */ +class Yaml +{ + /** + * Parses YAML into a PHP value. + * + * Usage: + * + * $array = Yaml::parse(file_get_contents('config.yml')); + * print_r($array); + * + * + * @param string $input A string containing YAML + * @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise + * @param bool $objectSupport True if object support is enabled, false otherwise + * @param bool $objectForMap True if maps should return a stdClass instead of array() + * + * @return mixed The YAML converted to a PHP value + * + * @throws ParseException If the YAML is not valid + */ + public static function parse($input, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false) + { + $yaml = new Parser(); + + return $yaml->parse($input, $exceptionOnInvalidType, $objectSupport, $objectForMap); + } + + /** + * Dumps a PHP array to a YAML string. + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. + * + * @param array $array PHP array + * @param int $inline The level where you switch to inline YAML + * @param int $indent The amount of spaces to use for indentation of nested nodes. + * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise + * @param bool $objectSupport true if object support is enabled, false otherwise + * + * @return string A YAML string representing the original PHP array + */ + public static function dump($array, $inline = 2, $indent = 4, $exceptionOnInvalidType = false, $objectSupport = false) + { + if ($indent < 1) { + throw new \InvalidArgumentException('The indentation must be greater than zero.'); + } + + $yaml = new Dumper(); + $yaml->setIndentation($indent); + + return $yaml->dump($array, $inline, 0, $exceptionOnInvalidType, $objectSupport); + } +} diff --git a/vendor/symfony/yaml/composer.json b/vendor/symfony/yaml/composer.json new file mode 100644 index 0000000..db1714c --- /dev/null +++ b/vendor/symfony/yaml/composer.json @@ -0,0 +1,33 @@ +{ + "name": "symfony/yaml", + "type": "library", + "description": "Symfony Yaml Component", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.5.9" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Yaml\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + } +} diff --git a/vendor/symfony/yaml/phpunit.xml.dist b/vendor/symfony/yaml/phpunit.xml.dist new file mode 100644 index 0000000..6bdbea1 --- /dev/null +++ b/vendor/symfony/yaml/phpunit.xml.dist @@ -0,0 +1,28 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + + diff --git a/vendor/zendframework/zendxml/.gitignore b/vendor/zendframework/zendxml/.gitignore new file mode 100644 index 0000000..0a4f6e2 --- /dev/null +++ b/vendor/zendframework/zendxml/.gitignore @@ -0,0 +1,5 @@ +composer.lock +vendor +.buildpath +.project +.settings diff --git a/vendor/zendframework/zendxml/.travis.yml b/vendor/zendframework/zendxml/.travis.yml new file mode 100644 index 0000000..877b065 --- /dev/null +++ b/vendor/zendframework/zendxml/.travis.yml @@ -0,0 +1,43 @@ +sudo: false + +language: php + +branches: + except: + - /^release-.*$/ + - /^ghgfk-.*$/ + +cache: + directories: + - $HOME/.composer/cache + +matrix: + allow_failures: + - php: hhvm +matrix: + fast_finish: true + include: + - php: 5.3 + - php: 5.4 + - php: 5.5 + env: + - EXECUTE_CS_CHECK=true + - php: 5.6 + - php: 7 + - php: hhvm + allow_failures: + - php: hhvm + +before_install: + - composer self-update + +install: + - travis_retry composer install --no-interaction --ignore-platform-reqs + +script: + - ./vendor/bin/phpunit -c ./tests + - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/phpcs --standard=PSR2 --ignore=tests/Bootstrap.php library tests ; fi + +notifications: + irc: "irc.freenode.org#zftalk.dev" + email: false diff --git a/vendor/zendframework/zendxml/CHANGELOG.md b/vendor/zendframework/zendxml/CHANGELOG.md new file mode 100644 index 0000000..bca2961 --- /dev/null +++ b/vendor/zendframework/zendxml/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +All notable changes to this project will be documented in this file, in reverse chronological order by release. + +## 1.0.2 - 2016-02-04 + +### Added + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- [#11](https://github.com/zendframework/ZendXml/pull/11) updates the + dependencies to PHP `^5.3.3 || ^7.0` and PHPUnit `^3.7 || ^4.0`, ensuring + better compatibility with other components, and with PHP 7. The test matrix + was also expanded to add PHP 7 as a required platform. diff --git a/vendor/zendframework/zendxml/LICENSE.md b/vendor/zendframework/zendxml/LICENSE.md new file mode 100644 index 0000000..141d3a2 --- /dev/null +++ b/vendor/zendframework/zendxml/LICENSE.md @@ -0,0 +1,12 @@ +Copyright (c) 2014-2015, Zend Technologies USA, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +- Neither the name of Zend Technologies USA, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/zendframework/zendxml/README.md b/vendor/zendframework/zendxml/README.md new file mode 100644 index 0000000..2c67008 --- /dev/null +++ b/vendor/zendframework/zendxml/README.md @@ -0,0 +1,50 @@ +ZendXml +======= + +An utility component for XML usage and best practices in PHP + +Installation +------------ + +You can install using: + +``` +curl -s https://getcomposer.org/installer | php +php composer.phar install +``` + +Notice that this library doesn't have any external dependencies, the usage of composer is for autoloading and standard purpose. + + +ZendXml\Security +---------------- + +This is a security component to prevent [XML eXternal Entity](https://www.owasp.org/index.php/XML_External_Entity_%28XXE%29_Processing) (XXE) and [XML Entity Expansion](http://projects.webappsec.org/w/page/13247002/XML%20Entity%20Expansion) (XEE) attacks on XML documents. + +The XXE attack is prevented disabling the load of external entities in the libxml library used by PHP, using the function [libxml_disable_entity_loader](http://www.php.net/manual/en/function.libxml-disable-entity-loader.php). + +The XEE attack is prevented looking inside the XML document for ENTITY usage. If the XML document uses ENTITY the library throw an Exception. + +We have two static methods to scan and load XML document from a string (scan) and from a file (scanFile). You can decide to get a SimpleXMLElement or DOMDocument as result, using the following use cases: + +```php +use ZendXml\Security as XmlSecurity; + +$xml = << + + test + +XML; + +// SimpleXML use case +$simplexml = XmlSecurity::scan($xml); +printf ("SimpleXMLElement: %s\n", ($simplexml instanceof \SimpleXMLElement) ? 'yes' : 'no'); + +// DOMDocument use case +$dom = new \DOMDocument('1.0'); +$dom = XmlSecurity::scan($xml, $dom); +printf ("DOMDocument: %s\n", ($dom instanceof \DOMDocument) ? 'yes' : 'no'); +``` + + diff --git a/vendor/zendframework/zendxml/composer.json b/vendor/zendframework/zendxml/composer.json new file mode 100644 index 0000000..d9efb22 --- /dev/null +++ b/vendor/zendframework/zendxml/composer.json @@ -0,0 +1,40 @@ +{ + "name": "zendframework/zendxml", + "description": "Utility library for XML usage, best practices, and security in PHP", + "type": "library", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "xml", + "security" + ], + "homepage": "http://packages.zendframework.com/", + "autoload": { + "psr-0": { + "ZendXml\\": "library/" + } + }, + "autoload-dev": { + "psr-4": { + "ZendTest\\Xml\\": "tests/ZendXmlTest/" + } + }, + "repositories": [ + { + "type": "composer", + "url": "http://packages.zendframework.com/" + } + ], + "require": { + "php": "^5.3.3 || ^7.0" + }, + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "require-dev": { + "phpunit/phpunit": "^3.7 || ^4.0", + "squizlabs/php_codesniffer": "^1.5" + } +} diff --git a/vendor/zendframework/zendxml/library/ZendXml/Exception/ExceptionInterface.php b/vendor/zendframework/zendxml/library/ZendXml/Exception/ExceptionInterface.php new file mode 100644 index 0000000..c55eb90 --- /dev/null +++ b/vendor/zendframework/zendxml/library/ZendXml/Exception/ExceptionInterface.php @@ -0,0 +1,14 @@ + 0) { + return true; + } + return false; + }, E_WARNING); + $result = $dom->loadXml($xml, LIBXML_NONET); + restore_error_handler(); + + if (!$result) { + // Entity load to previous setting + if (!self::isPhpFpm()) { + libxml_disable_entity_loader($loadEntities); + libxml_use_internal_errors($useInternalXmlErrors); + } + return false; + } + + // Scan for potential XEE attacks using ENTITY, if not PHP-FPM + if (!self::isPhpFpm()) { + foreach ($dom->childNodes as $child) { + if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) { + if ($child->entities->length > 0) { + throw new Exception\RuntimeException(self::ENTITY_DETECT); + } + } + } + } + + // Entity load to previous setting + if (!self::isPhpFpm()) { + libxml_disable_entity_loader($loadEntities); + libxml_use_internal_errors($useInternalXmlErrors); + } + + if (isset($simpleXml)) { + $result = simplexml_import_dom($dom); + if (!$result instanceof SimpleXMLElement) { + return false; + } + return $result; + } + return $dom; + } + + /** + * Scan XML file for potential XXE/XEE attacks + * + * @param string $file + * @param DOMDocument $dom + * @throws Exception\InvalidArgumentException + * @return SimpleXMLElement|DomDocument + */ + public static function scanFile($file, DOMDocument $dom = null) + { + if (!file_exists($file)) { + throw new Exception\InvalidArgumentException( + "The file $file specified doesn't exist" + ); + } + return self::scan(file_get_contents($file), $dom); + } + + /** + * Return true if PHP is running with PHP-FPM + * + * This method is mainly used to determine whether or not heuristic checks + * (vs libxml checks) should be made, due to threading issues in libxml; + * under php-fpm, threading becomes a concern. + * + * However, PHP versions 5.5.22+ and 5.6.6+ contain a patch to the + * libxml support in PHP that makes the libxml checks viable; in such + * versions, this method will return false to enforce those checks, which + * are more strict and accurate than the heuristic checks. + * + * @return boolean + */ + public static function isPhpFpm() + { + $isVulnerableVersion = ( + version_compare(PHP_VERSION, '5.5.22', 'lt') + || ( + version_compare(PHP_VERSION, '5.6', 'gte') + && version_compare(PHP_VERSION, '5.6.6', 'lt') + ) + ); + + if (substr(php_sapi_name(), 0, 3) === 'fpm' && $isVulnerableVersion) { + return true; + } + return false; + } + + /** + * Determine and return the string(s) to use for the $generator) { + $prefix = $generator('<' . '?xml'); + if (0 === strncmp($xml, $prefix, strlen($prefix))) { + return $encoding; + } + } + + // Fallback + return 'UTF-8'; + } + + /** + * Attempt to detect the specified XML encoding. + * + * Using the file's encoding, determines if an "encoding" attribute is + * present and well-formed in the XML declaration; if so, it returns a + * list with both the ASCII representation of that declaration and the + * original file encoding. + * + * If not, a list containing only the provided file encoding is returned. + * + * @param string $xml + * @param string $fileEncoding + * @return string[] Potential XML encodings + */ + protected static function detectXmlEncoding($xml, $fileEncoding) + { + $encodingMap = self::getAsciiEncodingMap(); + $generator = $encodingMap[$fileEncoding]; + $encAttr = $generator('encoding="'); + $quote = $generator('"'); + $close = $generator('>'); + + $closePos = strpos($xml, $close); + if (false === $closePos) { + return array($fileEncoding); + } + + $encPos = strpos($xml, $encAttr); + if (false === $encPos + || $encPos > $closePos + ) { + return array($fileEncoding); + } + + $encPos += strlen($encAttr); + $quotePos = strpos($xml, $quote, $encPos); + if (false === $quotePos) { + return array($fileEncoding); + } + + $encoding = self::substr($xml, $encPos, $quotePos); + return array( + // Following line works because we're only supporting 8-bit safe encodings at this time. + str_replace('\0', '', $encoding), // detected encoding + $fileEncoding, // file encoding + ); + } + + /** + * Return a list of BOM maps. + * + * Returns a list of common encoding -> BOM maps, along with the character + * length to compare against. + * + * @link https://en.wikipedia.org/wiki/Byte_order_mark + * @return array + */ + protected static function getBomMap() + { + return array( + array( + 'encoding' => 'UTF-32BE', + 'bom' => pack('CCCC', 0x00, 0x00, 0xfe, 0xff), + 'length' => 4, + ), + array( + 'encoding' => 'UTF-32LE', + 'bom' => pack('CCCC', 0xff, 0xfe, 0x00, 0x00), + 'length' => 4, + ), + array( + 'encoding' => 'GB-18030', + 'bom' => pack('CCCC', 0x84, 0x31, 0x95, 0x33), + 'length' => 4, + ), + array( + 'encoding' => 'UTF-16BE', + 'bom' => pack('CC', 0xfe, 0xff), + 'length' => 2, + ), + array( + 'encoding' => 'UTF-16LE', + 'bom' => pack('CC', 0xff, 0xfe), + 'length' => 2, + ), + array( + 'encoding' => 'UTF-8', + 'bom' => pack('CCC', 0xef, 0xbb, 0xbf), + 'length' => 3, + ), + ); + } + + /** + * Return a map of encoding => generator pairs. + * + * Returns a map of encoding => generator pairs, where the generator is a + * callable that accepts a string and returns the appropriate byte order + * sequence of that string for the encoding. + * + * @return array + */ + protected static function getAsciiEncodingMap() + { + return array( + 'UTF-32BE' => function ($ascii) { + return preg_replace('/(.)/', "\0\0\0\\1", $ascii); + }, + 'UTF-32LE' => function ($ascii) { + return preg_replace('/(.)/', "\\1\0\0\0", $ascii); + }, + 'UTF-32odd1' => function ($ascii) { + return preg_replace('/(.)/', "\0\\1\0\0", $ascii); + }, + 'UTF-32odd2' => function ($ascii) { + return preg_replace('/(.)/', "\0\0\\1\0", $ascii); + }, + 'UTF-16BE' => function ($ascii) { + return preg_replace('/(.)/', "\0\\1", $ascii); + }, + 'UTF-16LE' => function ($ascii) { + return preg_replace('/(.)/', "\\1\0", $ascii); + }, + 'UTF-8' => function ($ascii) { + return $ascii; + }, + 'GB-18030' => function ($ascii) { + return $ascii; + }, + ); + } + + /** + * Binary-safe substr. + * + * substr() is not binary-safe; this method loops by character to ensure + * multi-byte characters are aggregated correctly. + * + * @param string $string + * @param int $start + * @param int $end + * @return string + */ + protected static function substr($string, $start, $end) + { + $substr = ''; + for ($i = $start; $i < $end; $i += 1) { + $substr .= $string[$i]; + } + return $substr; + } +} diff --git a/vendor/zendframework/zendxml/tests/Bootstrap.php b/vendor/zendframework/zendxml/tests/Bootstrap.php new file mode 100644 index 0000000..a9d0e6a --- /dev/null +++ b/vendor/zendframework/zendxml/tests/Bootstrap.php @@ -0,0 +1,92 @@ +addDirectoryToWhitelist($zfCoreLibrary . '/' . $lastArg); + } elseif (is_file($zfCoreTests . '/' . $lastArg)) { + $codeCoverageFilter->addDirectoryToWhitelist(dirname($zfCoreLibrary . '/' . $lastArg)); + } else { + $codeCoverageFilter->addDirectoryToWhitelist($zfCoreLibrary); + } + + /* + * Omit from code coverage reports the contents of the tests directory + */ + $codeCoverageFilter->addDirectoryToBlacklist($zfCoreTests, ''); + $codeCoverageFilter->addDirectoryToBlacklist(PEAR_INSTALL_DIR, ''); + $codeCoverageFilter->addDirectoryToBlacklist(PHP_LIBDIR, ''); + + unset($codeCoverageFilter); +} + +/* + * Unset global variables that are no longer needed. + */ +unset($phpUnitVersion); diff --git a/vendor/zendframework/zendxml/tests/ZendXmlTest/MultibyteTest.php b/vendor/zendframework/zendxml/tests/ZendXmlTest/MultibyteTest.php new file mode 100644 index 0000000..165e8fa --- /dev/null +++ b/vendor/zendframework/zendxml/tests/ZendXmlTest/MultibyteTest.php @@ -0,0 +1,125 @@ + array('UTF-16LE', pack('CC', 0xff, 0xfe), 3), + 'UTF-16BE' => array('UTF-16BE', pack('CC', 0xfe, 0xff), 3), + 'UTF-32LE' => array('UTF-32LE', pack('CCCC', 0xff, 0xfe, 0x00, 0x00), 4), + 'UTF-32BE' => array('UTF-32BE', pack('CCCC', 0x00, 0x00, 0xfe, 0xff), 4), + ); + } + + public function getXmlWithXXE() + { + return << + +]> + + retrieved: &pocdata; + +XML; + } + + /** + * Invoke ZendXml\Security::heuristicScan with the provided XML. + * + * @param string $xml + * @return void + * @throws Exception\RuntimeException + */ + public function invokeHeuristicScan($xml) + { + $r = new ReflectionMethod('ZendXml\Security', 'heuristicScan'); + $r->setAccessible(true); + return $r->invoke(null, $xml); + } + + /** + * @dataProvider multibyteEncodings + * @group heuristicDetection + */ + public function testDetectsMultibyteXXEVectorsUnderFPMWithEncodedStringMissingBOM($encoding, $bom, $bomLength) + { + $xml = $this->getXmlWithXXE(); + $xml = str_replace('{ENCODING}', $encoding, $xml); + $xml = iconv('UTF-8', $encoding, $xml); + $this->assertNotSame(0, strncmp($xml, $bom, $bomLength)); + $this->setExpectedException('ZendXml\Exception\RuntimeException', 'ENTITY'); + $this->invokeHeuristicScan($xml); + } + + /** + * @dataProvider multibyteEncodings + */ + public function testDetectsMultibyteXXEVectorsUnderFPMWithEncodedStringUsingBOM($encoding, $bom) + { + $xml = $this->getXmlWithXXE(); + $xml = str_replace('{ENCODING}', $encoding, $xml); + $orig = iconv('UTF-8', $encoding, $xml); + $xml = $bom . $orig; + $this->setExpectedException('ZendXml\Exception\RuntimeException', 'ENTITY'); + $this->invokeHeuristicScan($xml); + } + + public function getXmlWithoutXXE() + { + return << + + retrieved: &pocdata; + +XML; + } + + /** + * @dataProvider multibyteEncodings + */ + public function testDoesNotFlagValidMultibyteXmlAsInvalidUnderFPM($encoding) + { + $xml = $this->getXmlWithoutXXE(); + $xml = str_replace('{ENCODING}', $encoding, $xml); + $xml = iconv('UTF-8', $encoding, $xml); + try { + $result = $this->invokeHeuristicScan($xml); + $this->assertNull($result); + } catch (\Exception $e) { + $this->fail('Security scan raised exception when it should not have'); + } + } + + /** + * @dataProvider multibyteEncodings + * @group mixedEncoding + */ + public function testDetectsXXEWhenXMLDocumentEncodingDiffersFromFileEncoding($encoding, $bom) + { + $xml = $this->getXmlWithXXE(); + $xml = str_replace('{ENCODING}', 'UTF-8', $xml); + $xml = iconv('UTF-8', $encoding, $xml); + $xml = $bom . $xml; + $this->setExpectedException('ZendXml\Exception\RuntimeException', 'ENTITY'); + $this->invokeHeuristicScan($xml); + } +} diff --git a/vendor/zendframework/zendxml/tests/ZendXmlTest/SecurityTest.php b/vendor/zendframework/zendxml/tests/ZendXmlTest/SecurityTest.php new file mode 100644 index 0000000..fa3b30b --- /dev/null +++ b/vendor/zendframework/zendxml/tests/ZendXmlTest/SecurityTest.php @@ -0,0 +1,135 @@ + +]> + + This result is &harmless; + +XML; + + $this->setExpectedException('ZendXml\Exception\RuntimeException'); + $result = XmlSecurity::scan($xml); + } + + public function testScanForXXE() + { + $file = tempnam(sys_get_temp_dir(), 'ZendXml_Security'); + file_put_contents($file, 'This is a remote content!'); + $xml = << + +]> + + &foo; + +XML; + + try { + $result = XmlSecurity::scan($xml); + } catch (Exception\RuntimeException $e) { + unlink($file); + return; + } + $this->fail('An expected exception has not been raised.'); + } + + public function testScanSimpleXmlResult() + { + $result = XmlSecurity::scan($this->getXml()); + $this->assertTrue($result instanceof SimpleXMLElement); + $this->assertEquals($result->result, 'test'); + } + + public function testScanDom() + { + $dom = new DOMDocument('1.0'); + $result = XmlSecurity::scan($this->getXml(), $dom); + $this->assertTrue($result instanceof DOMDocument); + $node = $result->getElementsByTagName('result')->item(0); + $this->assertEquals($node->nodeValue, 'test'); + } + + public function testScanInvalidXml() + { + $xml = <<test +XML; + + $result = XmlSecurity::scan($xml); + $this->assertFalse($result); + } + + public function testScanInvalidXmlDom() + { + $xml = <<test +XML; + + $dom = new DOMDocument('1.0'); + $result = XmlSecurity::scan($xml, $dom); + $this->assertFalse($result); + } + + public function testScanFile() + { + $file = tempnam(sys_get_temp_dir(), 'ZendXml_Security'); + file_put_contents($file, $this->getXml()); + + $result = XmlSecurity::scanFile($file); + $this->assertTrue($result instanceof SimpleXMLElement); + $this->assertEquals($result->result, 'test'); + unlink($file); + } + + public function testScanXmlWithDTD() + { + $xml = << + + +]> + + test + +XML; + + $dom = new DOMDocument('1.0'); + $result = XmlSecurity::scan($xml, $dom); + $this->assertTrue($result instanceof DOMDocument); + $this->assertTrue($result->validate()); + } + + protected function getXml() + { + return << + + test + +XML; + } +} diff --git a/vendor/zendframework/zendxml/tests/phpunit.xml.dist b/vendor/zendframework/zendxml/tests/phpunit.xml.dist new file mode 100755 index 0000000..069784b --- /dev/null +++ b/vendor/zendframework/zendxml/tests/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + ./ZendXmlTest + ./ZendXmlTest/TestAsset + + + + + + + + + + + + + + ./ZendXmlTest + ../vendor + + + + + + +