From 84e38a5663d9f884fdcc489a737528433f07f42e Mon Sep 17 00:00:00 2001 From: Brian Huisman Date: Thu, 20 Apr 2023 10:47:11 -0400 Subject: [PATCH] Re-upload 3rd party libraries --- .gitignore | 8 +- orcinus/admin.php | 8 +- orcinus/config.php | 40 +- orcinus/crawler.php | 8 +- orcinus/geoip2/README.md | 8 + orcinus/js/mustache/README.md | 621 ++ orcinus/libcurlemu/README.txt | 99 + orcinus/libcurlemu/class_HTTPRetriever.php | 1162 ++++ orcinus/libcurlemu/example.php | 35 + orcinus/libcurlemu/libcurlemu.inc.php | 111 + orcinus/libcurlemu/libcurlexternal.inc.php | 664 +++ orcinus/libcurlemu/libcurlnative.inc.php | 453 ++ orcinus/mustache/.gitattributes | 8 + orcinus/mustache/LICENSE | 21 + orcinus/mustache/README.md | 73 + orcinus/mustache/bin/build_bootstrap.php | 178 + orcinus/mustache/composer.json | 25 + orcinus/mustache/src/Mustache/Autoloader.php | 88 + orcinus/mustache/src/Mustache/Cache.php | 43 + .../src/Mustache/Cache/AbstractCache.php | 60 + .../src/Mustache/Cache/FilesystemCache.php | 161 + .../mustache/src/Mustache/Cache/NoopCache.php | 47 + orcinus/mustache/src/Mustache/Compiler.php | 689 +++ orcinus/mustache/src/Mustache/Context.php | 242 + orcinus/mustache/src/Mustache/Engine.php | 829 +++ orcinus/mustache/src/Mustache/Exception.php | 18 + .../Exception/InvalidArgumentException.php | 18 + .../src/Mustache/Exception/LogicException.php | 18 + .../Mustache/Exception/RuntimeException.php | 18 + .../Mustache/Exception/SyntaxException.php | 41 + .../Exception/UnknownFilterException.php | 38 + .../Exception/UnknownHelperException.php | 38 + .../Exception/UnknownTemplateException.php | 38 + .../src/Mustache/HelperCollection.php | 172 + .../mustache/src/Mustache/LambdaHelper.php | 76 + orcinus/mustache/src/Mustache/Loader.php | 27 + .../src/Mustache/Loader/ArrayLoader.php | 79 + .../src/Mustache/Loader/CascadingLoader.php | 69 + .../src/Mustache/Loader/FilesystemLoader.php | 135 + .../src/Mustache/Loader/InlineLoader.php | 123 + .../src/Mustache/Loader/MutableLoader.php | 31 + .../Loader/ProductionFilesystemLoader.php | 86 + .../src/Mustache/Loader/StringLoader.php | 39 + orcinus/mustache/src/Mustache/Logger.php | 126 + .../src/Mustache/Logger/AbstractLogger.php | 121 + .../src/Mustache/Logger/StreamLogger.php | 194 + orcinus/mustache/src/Mustache/Parser.php | 317 + orcinus/mustache/src/Mustache/Source.php | 40 + .../src/Mustache/Source/FilesystemSource.php | 77 + orcinus/mustache/src/Mustache/Template.php | 180 + orcinus/mustache/src/Mustache/Tokenizer.php | 378 ++ orcinus/pdfparser/.php-cs-fixer.php | 31 + orcinus/pdfparser/LICENSE.txt | 165 + orcinus/pdfparser/Makefile | 18 + orcinus/pdfparser/README.md | 58 + orcinus/pdfparser/alt_autoload.php-dist | 75 + orcinus/pdfparser/composer.json | 37 + orcinus/pdfparser/doc/CustomConfig.md | 65 + orcinus/pdfparser/doc/Developer.md | 57 + orcinus/pdfparser/doc/Usage.md | 173 + orcinus/pdfparser/phpunit-windows.xml | 21 + .../pdfparser/src/Smalot/PdfParser/Config.php | 154 + .../src/Smalot/PdfParser/Document.php | 306 + .../src/Smalot/PdfParser/Element.php | 150 + .../Smalot/PdfParser/Element/ElementArray.php | 139 + .../PdfParser/Element/ElementBoolean.php | 75 + .../Smalot/PdfParser/Element/ElementDate.php | 139 + .../Smalot/PdfParser/Element/ElementHexa.php | 85 + .../PdfParser/Element/ElementMissing.php | 66 + .../Smalot/PdfParser/Element/ElementName.php | 69 + .../Smalot/PdfParser/Element/ElementNull.php | 71 + .../PdfParser/Element/ElementNumeric.php | 62 + .../PdfParser/Element/ElementString.php | 93 + .../PdfParser/Element/ElementStruct.php | 75 + .../Smalot/PdfParser/Element/ElementXRef.php | 98 + .../src/Smalot/PdfParser/Encoding.php | 157 + .../PdfParser/Encoding/AbstractEncoding.php | 8 + .../PdfParser/Encoding/EncodingLocator.php | 17 + .../PdfParser/Encoding/ISOLatin1Encoding.php | 76 + .../PdfParser/Encoding/ISOLatin9Encoding.php | 76 + .../PdfParser/Encoding/MacRomanEncoding.php | 80 + .../PdfParser/Encoding/PostScriptGlyphs.php | 1099 ++++ .../PdfParser/Encoding/StandardEncoding.php | 76 + .../PdfParser/Encoding/WinAnsiEncoding.php | 76 + .../Exception/EncodingNotFoundException.php | 7 + .../pdfparser/src/Smalot/PdfParser/Font.php | 664 +++ .../PdfParser/Font/FontCIDFontType0.php | 42 + .../PdfParser/Font/FontCIDFontType2.php | 42 + .../Smalot/PdfParser/Font/FontTrueType.php | 42 + .../src/Smalot/PdfParser/Font/FontType0.php | 42 + .../src/Smalot/PdfParser/Font/FontType1.php | 42 + .../src/Smalot/PdfParser/Font/FontType3.php | 42 + .../pdfparser/src/Smalot/PdfParser/Header.php | 194 + .../src/Smalot/PdfParser/PDFObject.php | 779 +++ .../pdfparser/src/Smalot/PdfParser/Page.php | 953 +++ .../pdfparser/src/Smalot/PdfParser/Pages.php | 73 + .../pdfparser/src/Smalot/PdfParser/Parser.php | 327 ++ .../Smalot/PdfParser/RawData/FilterHelper.php | 396 ++ .../PdfParser/RawData/RawDataParser.php | 902 +++ .../src/Smalot/PdfParser/XObject/Form.php | 51 + .../src/Smalot/PdfParser/XObject/Image.php | 47 + orcinus/phpmailer/COMMITMENT | 46 + orcinus/phpmailer/LICENSE | 502 ++ orcinus/phpmailer/README.md | 230 + orcinus/phpmailer/SECURITY.md | 37 + orcinus/phpmailer/VERSION | 1 + orcinus/phpmailer/composer.json | 78 + orcinus/phpmailer/get_oauth_token.php | 182 + .../phpmailer/language/phpmailer.lang-af.php | 26 + .../phpmailer/language/phpmailer.lang-ar.php | 27 + .../phpmailer/language/phpmailer.lang-az.php | 27 + .../phpmailer/language/phpmailer.lang-ba.php | 27 + .../phpmailer/language/phpmailer.lang-be.php | 27 + .../phpmailer/language/phpmailer.lang-bg.php | 27 + .../phpmailer/language/phpmailer.lang-ca.php | 27 + .../phpmailer/language/phpmailer.lang-cs.php | 28 + .../phpmailer/language/phpmailer.lang-da.php | 35 + .../phpmailer/language/phpmailer.lang-de.php | 28 + .../phpmailer/language/phpmailer.lang-el.php | 33 + .../phpmailer/language/phpmailer.lang-eo.php | 26 + .../phpmailer/language/phpmailer.lang-es.php | 31 + .../phpmailer/language/phpmailer.lang-et.php | 28 + .../phpmailer/language/phpmailer.lang-fa.php | 28 + .../phpmailer/language/phpmailer.lang-fi.php | 28 + .../phpmailer/language/phpmailer.lang-fo.php | 27 + .../phpmailer/language/phpmailer.lang-fr.php | 38 + .../phpmailer/language/phpmailer.lang-gl.php | 27 + .../phpmailer/language/phpmailer.lang-he.php | 27 + .../phpmailer/language/phpmailer.lang-hi.php | 35 + .../phpmailer/language/phpmailer.lang-hr.php | 27 + .../phpmailer/language/phpmailer.lang-hu.php | 27 + .../phpmailer/language/phpmailer.lang-hy.php | 27 + .../phpmailer/language/phpmailer.lang-id.php | 31 + .../phpmailer/language/phpmailer.lang-it.php | 28 + .../phpmailer/language/phpmailer.lang-ja.php | 29 + .../phpmailer/language/phpmailer.lang-ka.php | 27 + .../phpmailer/language/phpmailer.lang-ko.php | 27 + .../phpmailer/language/phpmailer.lang-lt.php | 27 + .../phpmailer/language/phpmailer.lang-lv.php | 27 + .../phpmailer/language/phpmailer.lang-mg.php | 27 + .../phpmailer/language/phpmailer.lang-mn.php | 27 + .../phpmailer/language/phpmailer.lang-ms.php | 27 + .../phpmailer/language/phpmailer.lang-nb.php | 26 + .../phpmailer/language/phpmailer.lang-nl.php | 34 + .../phpmailer/language/phpmailer.lang-pl.php | 26 + .../phpmailer/language/phpmailer.lang-pt.php | 27 + .../language/phpmailer.lang-pt_br.php | 38 + .../phpmailer/language/phpmailer.lang-ro.php | 33 + .../phpmailer/language/phpmailer.lang-ru.php | 28 + .../phpmailer/language/phpmailer.lang-sk.php | 30 + .../phpmailer/language/phpmailer.lang-sl.php | 36 + .../phpmailer/language/phpmailer.lang-sr.php | 28 + .../language/phpmailer.lang-sr_latn.php | 28 + .../phpmailer/language/phpmailer.lang-sv.php | 27 + .../phpmailer/language/phpmailer.lang-tl.php | 28 + .../phpmailer/language/phpmailer.lang-tr.php | 31 + .../phpmailer/language/phpmailer.lang-uk.php | 28 + .../phpmailer/language/phpmailer.lang-vi.php | 27 + .../phpmailer/language/phpmailer.lang-zh.php | 29 + .../language/phpmailer.lang-zh_cn.php | 29 + orcinus/phpmailer/src/DSNConfigurator.php | 247 + orcinus/phpmailer/src/Exception.php | 40 + orcinus/phpmailer/src/OAuth.php | 139 + orcinus/phpmailer/src/OAuthTokenProvider.php | 44 + orcinus/phpmailer/src/PHPMailer.php | 5126 +++++++++++++++++ orcinus/phpmailer/src/POP3.php | 467 ++ orcinus/phpmailer/src/SMTP.php | 1466 +++++ orcinus/search.php | 32 + 168 files changed, 26854 insertions(+), 51 deletions(-) create mode 100644 orcinus/geoip2/README.md create mode 100644 orcinus/js/mustache/README.md create mode 100644 orcinus/libcurlemu/README.txt create mode 100644 orcinus/libcurlemu/class_HTTPRetriever.php create mode 100644 orcinus/libcurlemu/example.php create mode 100644 orcinus/libcurlemu/libcurlemu.inc.php create mode 100644 orcinus/libcurlemu/libcurlexternal.inc.php create mode 100644 orcinus/libcurlemu/libcurlnative.inc.php create mode 100644 orcinus/mustache/.gitattributes create mode 100644 orcinus/mustache/LICENSE create mode 100644 orcinus/mustache/README.md create mode 100644 orcinus/mustache/bin/build_bootstrap.php create mode 100644 orcinus/mustache/composer.json create mode 100644 orcinus/mustache/src/Mustache/Autoloader.php create mode 100644 orcinus/mustache/src/Mustache/Cache.php create mode 100644 orcinus/mustache/src/Mustache/Cache/AbstractCache.php create mode 100644 orcinus/mustache/src/Mustache/Cache/FilesystemCache.php create mode 100644 orcinus/mustache/src/Mustache/Cache/NoopCache.php create mode 100644 orcinus/mustache/src/Mustache/Compiler.php create mode 100644 orcinus/mustache/src/Mustache/Context.php create mode 100644 orcinus/mustache/src/Mustache/Engine.php create mode 100644 orcinus/mustache/src/Mustache/Exception.php create mode 100644 orcinus/mustache/src/Mustache/Exception/InvalidArgumentException.php create mode 100644 orcinus/mustache/src/Mustache/Exception/LogicException.php create mode 100644 orcinus/mustache/src/Mustache/Exception/RuntimeException.php create mode 100644 orcinus/mustache/src/Mustache/Exception/SyntaxException.php create mode 100644 orcinus/mustache/src/Mustache/Exception/UnknownFilterException.php create mode 100644 orcinus/mustache/src/Mustache/Exception/UnknownHelperException.php create mode 100644 orcinus/mustache/src/Mustache/Exception/UnknownTemplateException.php create mode 100644 orcinus/mustache/src/Mustache/HelperCollection.php create mode 100644 orcinus/mustache/src/Mustache/LambdaHelper.php create mode 100644 orcinus/mustache/src/Mustache/Loader.php create mode 100644 orcinus/mustache/src/Mustache/Loader/ArrayLoader.php create mode 100644 orcinus/mustache/src/Mustache/Loader/CascadingLoader.php create mode 100644 orcinus/mustache/src/Mustache/Loader/FilesystemLoader.php create mode 100644 orcinus/mustache/src/Mustache/Loader/InlineLoader.php create mode 100644 orcinus/mustache/src/Mustache/Loader/MutableLoader.php create mode 100644 orcinus/mustache/src/Mustache/Loader/ProductionFilesystemLoader.php create mode 100644 orcinus/mustache/src/Mustache/Loader/StringLoader.php create mode 100644 orcinus/mustache/src/Mustache/Logger.php create mode 100644 orcinus/mustache/src/Mustache/Logger/AbstractLogger.php create mode 100644 orcinus/mustache/src/Mustache/Logger/StreamLogger.php create mode 100644 orcinus/mustache/src/Mustache/Parser.php create mode 100644 orcinus/mustache/src/Mustache/Source.php create mode 100644 orcinus/mustache/src/Mustache/Source/FilesystemSource.php create mode 100644 orcinus/mustache/src/Mustache/Template.php create mode 100644 orcinus/mustache/src/Mustache/Tokenizer.php create mode 100644 orcinus/pdfparser/.php-cs-fixer.php create mode 100644 orcinus/pdfparser/LICENSE.txt create mode 100644 orcinus/pdfparser/Makefile create mode 100644 orcinus/pdfparser/README.md create mode 100644 orcinus/pdfparser/alt_autoload.php-dist create mode 100644 orcinus/pdfparser/composer.json create mode 100644 orcinus/pdfparser/doc/CustomConfig.md create mode 100644 orcinus/pdfparser/doc/Developer.md create mode 100644 orcinus/pdfparser/doc/Usage.md create mode 100644 orcinus/pdfparser/phpunit-windows.xml create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Config.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Document.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Element.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementArray.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementBoolean.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementDate.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementHexa.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementMissing.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementName.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementNull.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementNumeric.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementString.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementStruct.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementXRef.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Encoding.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Encoding/AbstractEncoding.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Encoding/EncodingLocator.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Encoding/ISOLatin1Encoding.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Encoding/ISOLatin9Encoding.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Encoding/MacRomanEncoding.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Encoding/PostScriptGlyphs.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Encoding/StandardEncoding.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Encoding/WinAnsiEncoding.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Exception/EncodingNotFoundException.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Font.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Font/FontCIDFontType0.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Font/FontCIDFontType2.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Font/FontTrueType.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Font/FontType0.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Font/FontType1.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Font/FontType3.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Header.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/PDFObject.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Page.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Pages.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/Parser.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/RawData/FilterHelper.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/RawData/RawDataParser.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/XObject/Form.php create mode 100644 orcinus/pdfparser/src/Smalot/PdfParser/XObject/Image.php create mode 100644 orcinus/phpmailer/COMMITMENT create mode 100644 orcinus/phpmailer/LICENSE create mode 100644 orcinus/phpmailer/README.md create mode 100644 orcinus/phpmailer/SECURITY.md create mode 100644 orcinus/phpmailer/VERSION create mode 100644 orcinus/phpmailer/composer.json create mode 100644 orcinus/phpmailer/get_oauth_token.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-af.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-ar.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-az.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-ba.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-be.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-bg.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-ca.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-cs.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-da.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-de.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-el.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-eo.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-es.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-et.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-fa.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-fi.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-fo.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-fr.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-gl.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-he.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-hi.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-hr.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-hu.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-hy.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-id.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-it.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-ja.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-ka.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-ko.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-lt.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-lv.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-mg.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-mn.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-ms.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-nb.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-nl.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-pl.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-pt.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-pt_br.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-ro.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-ru.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-sk.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-sl.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-sr.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-sr_latn.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-sv.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-tl.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-tr.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-uk.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-vi.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-zh.php create mode 100644 orcinus/phpmailer/language/phpmailer.lang-zh_cn.php create mode 100644 orcinus/phpmailer/src/DSNConfigurator.php create mode 100644 orcinus/phpmailer/src/Exception.php create mode 100644 orcinus/phpmailer/src/OAuth.php create mode 100644 orcinus/phpmailer/src/OAuthTokenProvider.php create mode 100644 orcinus/phpmailer/src/PHPMailer.php create mode 100644 orcinus/phpmailer/src/POP3.php create mode 100644 orcinus/phpmailer/src/SMTP.php diff --git a/.gitignore b/.gitignore index d51bae8..171c37a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,4 @@ orcinus/GeoIP2/GeoLite2-Country.mmdb orcinus/GeoIP2/geoip2.phar orcinus/GeoIP2/COPYRIGHT.txt orcinus/config.ini.php -*.7z -orcinus/libcurlemu/class_HTTPRetriever.php -orcinus/libcurlemu/example.php -orcinus/libcurlemu/libcurlemu.inc.php -orcinus/libcurlemu/libcurlexternal.inc.php -orcinus/libcurlemu/libcurlnative.inc.php -orcinus/libcurlemu/README.txt +*.7z \ No newline at end of file diff --git a/orcinus/admin.php b/orcinus/admin.php index 7bafe92..10422c9 100644 --- a/orcinus/admin.php +++ b/orcinus/admin.php @@ -50,13 +50,13 @@ function OS_countUp($time) { // ***** Load Maxmind GeoIP2 if (!class_exists('GeoIp2\Database\Reader')) { - if (file_exists(__DIR__.'/GeoIP2/geoip2.phar')) { - include __DIR__.'/GeoIP2/geoip2.phar'; + if (file_exists(__DIR__.'/geoip2/geoip2.phar')) { + include __DIR__.'/geoip2/geoip2.phar'; } } if (class_exists('GeoIp2\Database\Reader')) { - if (file_exists(__DIR__.'/GeoIP2/GeoLite2-Country.mmdb')) - $_GEOIP2 = new GeoIp2\Database\Reader(__DIR__.'/GeoIP2/GeoLite2-Country.mmdb'); + if (file_exists(__DIR__.'/geoip2/GeoLite2-Country.mmdb')) + $_GEOIP2 = new GeoIp2\Database\Reader(__DIR__.'/geoip2/GeoLite2-Country.mmdb'); } else $_GEOIP2 = false; diff --git a/orcinus/config.php b/orcinus/config.php index 972c66c..3a1b3e7 100644 --- a/orcinus/config.php +++ b/orcinus/config.php @@ -395,10 +395,10 @@ if (!$_ODATA['admin_from']) { // ***** Load and Initialize PHPMailer if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) { - if (file_exists(__DIR__.'/PHPMailer/PHPMailer.php')) { - include __DIR__.'/PHPMailer/PHPMailer.php'; - include __DIR__.'/PHPMailer/Exception.php'; - include __DIR__.'/PHPMailer/SMTP.php'; + if (file_exists(__DIR__.'/phpmailer/src/PHPMailer.php')) { + include __DIR__.'/phpmailer/src/PHPMailer.php'; + include __DIR__.'/phpmailer/src/Exception.php'; + include __DIR__.'/phpmailer/src/SMTP.php'; } } if (class_exists('PHPMailer\PHPMailer\PHPMailer')) { @@ -571,38 +571,6 @@ if (!$_ODATA['s_result_template']) { '); } -// {{{{{ Initialize the Mustache templating engine -class OS_Mustache { - public $errors; - public $version; - public $searchable; - - function __construct() { - global $_ODATA; - - $this->version = $_ODATA['version']; - } - - function addError($text) { - if (!$this->errors) { - $this->errors = new stdClass(); - $this->errors->error_list = array(); - } - $this->errors->error_list[] = $text; - } - - // We'll only autoload the Mustache engine if we need it - function render() { - global $_ODATA; - - require_once __DIR__.'/Mustache/Autoloader.php'; - Mustache_Autoloader::register(); - - $output = new Mustache_Engine(array('entity_flags' => ENT_QUOTES)); - echo $output->render($_ODATA['s_result_template'], $this); - } -} - // Purge entries from the search query log older than // 's_limit_query_log' ago diff --git a/orcinus/crawler.php b/orcinus/crawler.php index eede15b..6d81cb6 100644 --- a/orcinus/crawler.php +++ b/orcinus/crawler.php @@ -658,15 +658,15 @@ foreach ($_RDATA['sp_entity'] as $key => $value) // ***** Load PDF parser if (!class_exists('\Smalot\PdfParser\Parser')) - if (file_exists(__DIR__.'/PdfParser/alt_autoload.php-dist')) - include __DIR__.'/PdfParser/alt_autoload.php-dist'; + if (file_exists(__DIR__.'/pdfparser/alt_autoload.php-dist')) + include __DIR__.'/pdfparser/alt_autoload.php-dist'; if (class_exists('\Smalot\PdfParser\Parser')) { $config = new \Smalot\PdfParser\Config(); $config->setRetainImageContent(false); $config->setDecodeMemoryLimit(16777216); $_PDF = new \Smalot\PdfParser\Parser([], $config); } else { - OS_crawlLog('Could not include \'PdfParser\'; PDFs will not be indexed', 1); + OS_crawlLog('Could not include \'PDFParser\'; PDFs will not be indexed', 1); $_PDF = false; } @@ -1307,7 +1307,7 @@ while ($_cURL && count($_RDATA['sp_queue'])) { // Discard the PDF text if it contains Unicode control // characters; some of these might be simple PDF ligatures - // but PdfParser doesn't support them; any content that + // but PDFParser doesn't support them; any content that // contains these is usually mostly gobbledegook if (strpos($data['content'], "\u{3}") === false && strpos($data['content'], "\u{2}") === false && diff --git a/orcinus/geoip2/README.md b/orcinus/geoip2/README.md new file mode 100644 index 0000000..d44db02 --- /dev/null +++ b/orcinus/geoip2/README.md @@ -0,0 +1,8 @@ +# Maxmind GeoIP2 Geolocation + +To enable the Geolocation service for items in the Query Log, follow the steps below: + +1. Download the latest Maxmind GeoIP2 .phar file from the Github, and place it in the same directory as this README file: https://github.com/maxmind/GeoIP2-php/releases +2. Login at the Maxmind website; account registration is free: https://www.maxmind.com/en/account/login +3. Navigate to the "Downloads" area of your Maxmind account, and download the GeoLite Country (not CSV) GZIP package. +4. Unzip the 'GeoLite2-Country.mmdb' file and place it in the same directory as this README.txt file. diff --git a/orcinus/js/mustache/README.md b/orcinus/js/mustache/README.md new file mode 100644 index 0000000..127dfe1 --- /dev/null +++ b/orcinus/js/mustache/README.md @@ -0,0 +1,621 @@ +# mustache.js - Logic-less {{mustache}} templates with JavaScript + +> What could be more logical awesome than no logic at all? + +[![Build Status](https://travis-ci.org/janl/mustache.js.svg?branch=master)](https://travis-ci.org/janl/mustache.js) + +[mustache.js](http://github.com/janl/mustache.js) is a zero-dependency implementation of the [mustache](http://mustache.github.com/) template system in JavaScript. + +[Mustache](http://mustache.github.com/) is a logic-less template syntax. It can be used for HTML, config files, source code - anything. It works by expanding tags in a template using values provided in a hash or object. + +We call it "logic-less" because there are no if statements, else clauses, or for loops. Instead there are only tags. Some tags are replaced with a value, some nothing, and others a series of values. + +For a language-agnostic overview of mustache's template syntax, see the `mustache(5)` [manpage](http://mustache.github.com/mustache.5.html). + +## Where to use mustache.js? + +You can use mustache.js to render mustache templates anywhere you can use JavaScript. This includes web browsers, server-side environments such as [Node.js](http://nodejs.org/), and [CouchDB](http://couchdb.apache.org/) views. + +mustache.js ships with support for the [CommonJS](http://www.commonjs.org/) module API, the [Asynchronous Module Definition](https://github.com/amdjs/amdjs-api/wiki/AMD) API (AMD) and [ECMAScript modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules). + +In addition to being a package to be used programmatically, you can use it as a [command line tool](#command-line-tool). + +And this will be your templates after you use Mustache: + +!['stache](https://cloud.githubusercontent.com/assets/288977/8779228/a3cf700e-2f02-11e5-869a-300312fb7a00.gif) + +## Install + +You can get Mustache via [npm](http://npmjs.com). + +```bash +$ npm install mustache --save +``` + +## Usage + +Below is a quick example how to use mustache.js: + +```js +var view = { + title: "Joe", + calc: function () { + return 2 + 4; + } +}; + +var output = Mustache.render("{{title}} spends {{calc}}", view); +``` + +In this example, the `Mustache.render` function takes two parameters: 1) the [mustache](http://mustache.github.com/) template and 2) a `view` object that contains the data and code needed to render the template. + +## Templates + +A [mustache](http://mustache.github.com/) template is a string that contains any number of mustache tags. Tags are indicated by the double mustaches that surround them. `{{person}}` is a tag, as is `{{#person}}`. In both examples we refer to `person` as the tag's key. There are several types of tags available in mustache.js, described below. + +There are several techniques that can be used to load templates and hand them to mustache.js, here are two of them: + +#### Include Templates + +If you need a template for a dynamic part in a static website, you can consider including the template in the static HTML file to avoid loading templates separately. Here's a small example: + +```js +// file: render.js + +function renderHello() { + var template = document.getElementById('template').innerHTML; + var rendered = Mustache.render(template, { name: 'Luke' }); + document.getElementById('target').innerHTML = rendered; +} +``` + +```html + + +
Loading...
+ + + + + + +``` + +#### Load External Templates + +If your templates reside in individual files, you can load them asynchronously and render them when they arrive. Another example using [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch): + +```js +function renderHello() { + fetch('template.mustache') + .then((response) => response.text()) + .then((template) => { + var rendered = Mustache.render(template, { name: 'Luke' }); + document.getElementById('target').innerHTML = rendered; + }); +} +``` + +### Variables + +The most basic tag type is a simple variable. A `{{name}}` tag renders the value of the `name` key in the current context. If there is no such key, nothing is rendered. + +All variables are HTML-escaped by default. If you want to render unescaped HTML, use the triple mustache: `{{{name}}}`. You can also use `&` to unescape a variable. + +If you'd like to change HTML-escaping behavior globally (for example, to template non-HTML formats), you can override Mustache's escape function. For example, to disable all escaping: `Mustache.escape = function(text) {return text;};`. + +If you want `{{name}}` _not_ to be interpreted as a mustache tag, but rather to appear exactly as `{{name}}` in the output, you must change and then restore the default delimiter. See the [Custom Delimiters](#custom-delimiters) section for more information. + +View: + +```json +{ + "name": "Chris", + "company": "GitHub" +} +``` + +Template: + +``` +* {{name}} +* {{age}} +* {{company}} +* {{{company}}} +* {{&company}} +{{=<% %>=}} +* {{company}} +<%={{ }}=%> +``` + +Output: + +```html +* Chris +* +* <b>GitHub</b> +* GitHub +* GitHub +* {{company}} +``` + +JavaScript's dot notation may be used to access keys that are properties of objects in a view. + +View: + +```json +{ + "name": { + "first": "Michael", + "last": "Jackson" + }, + "age": "RIP" +} +``` + +Template: + +```html +* {{name.first}} {{name.last}} +* {{age}} +``` + +Output: + +```html +* Michael Jackson +* RIP +``` + +### Sections + +Sections render blocks of text zero or more times, depending on the value of the key in the current context. + +A section begins with a pound and ends with a slash. That is, `{{#person}}` begins a `person` section, while `{{/person}}` ends it. The text between the two tags is referred to as that section's "block". + +The behavior of the section is determined by the value of the key. + +#### False Values or Empty Lists + +If the `person` key does not exist, or exists and has a value of `null`, `undefined`, `false`, `0`, or `NaN`, or is an empty string or an empty list, the block will not be rendered. + +View: + +```json +{ + "person": false +} +``` + +Template: + +```html +Shown. +{{#person}} +Never shown! +{{/person}} +``` + +Output: + +```html +Shown. +``` + +#### Non-Empty Lists + +If the `person` key exists and is not `null`, `undefined`, or `false`, and is not an empty list the block will be rendered one or more times. + +When the value is a list, the block is rendered once for each item in the list. The context of the block is set to the current item in the list for each iteration. In this way we can loop over collections. + +View: + +```json +{ + "stooges": [ + { "name": "Moe" }, + { "name": "Larry" }, + { "name": "Curly" } + ] +} +``` + +Template: + +```html +{{#stooges}} +{{name}} +{{/stooges}} +``` + +Output: + +```html +Moe +Larry +Curly +``` + +When looping over an array of strings, a `.` can be used to refer to the current item in the list. + +View: + +```json +{ + "musketeers": ["Athos", "Aramis", "Porthos", "D'Artagnan"] +} +``` + +Template: + +```html +{{#musketeers}} +* {{.}} +{{/musketeers}} +``` + +Output: + +```html +* Athos +* Aramis +* Porthos +* D'Artagnan +``` + +If the value of a section variable is a function, it will be called in the context of the current item in the list on each iteration. + +View: + +```js +{ + "beatles": [ + { "firstName": "John", "lastName": "Lennon" }, + { "firstName": "Paul", "lastName": "McCartney" }, + { "firstName": "George", "lastName": "Harrison" }, + { "firstName": "Ringo", "lastName": "Starr" } + ], + "name": function () { + return this.firstName + " " + this.lastName; + } +} +``` + +Template: + +```html +{{#beatles}} +* {{name}} +{{/beatles}} +``` + +Output: + +```html +* John Lennon +* Paul McCartney +* George Harrison +* Ringo Starr +``` + +#### Functions + +If the value of a section key is a function, it is called with the section's literal block of text, un-rendered, as its first argument. The second argument is a special rendering function that uses the current view as its view argument. It is called in the context of the current view object. + +View: + +```js +{ + "name": "Tater", + "bold": function () { + return function (text, render) { + return "" + render(text) + ""; + } + } +} +``` + +Template: + +```html +{{#bold}}Hi {{name}}.{{/bold}} +``` + +Output: + +```html +Hi Tater. +``` + +### Inverted Sections + +An inverted section opens with `{{^section}}` instead of `{{#section}}`. The block of an inverted section is rendered only if the value of that section's tag is `null`, `undefined`, `false`, *falsy* or an empty list. + +View: + +```json +{ + "repos": [] +} +``` + +Template: + +```html +{{#repos}}{{name}}{{/repos}} +{{^repos}}No repos :({{/repos}} +``` + +Output: + +```html +No repos :( +``` + +### Comments + +Comments begin with a bang and are ignored. The following template: + +```html +

Today{{! ignore me }}.

+``` + +Will render as follows: + +```html +

Today.

+``` + +Comments may contain newlines. + +### Partials + +Partials begin with a greater than sign, like {{> box}}. + +Partials are rendered at runtime (as opposed to compile time), so recursive partials are possible. Just avoid infinite loops. + +They also inherit the calling context. Whereas in ERB you may have this: + +```html+erb +<%= partial :next_more, :start => start, :size => size %> +``` + +Mustache requires only this: + +```html +{{> next_more}} +``` + +Why? Because the `next_more.mustache` file will inherit the `size` and `start` variables from the calling context. In this way you may want to think of partials as includes, imports, template expansion, nested templates, or subtemplates, even though those aren't literally the case here. + + +For example, this template and partial: + + base.mustache: +

Names

+ {{#names}} + {{> user}} + {{/names}} + + user.mustache: + {{name}} + +Can be thought of as a single, expanded template: + +```html +

Names

+{{#names}} + {{name}} +{{/names}} +``` + +In mustache.js an object of partials may be passed as the third argument to `Mustache.render`. The object should be keyed by the name of the partial, and its value should be the partial text. + +```js +Mustache.render(template, view, { + user: userTemplate +}); +``` + +### Custom Delimiters + +Custom delimiters can be used in place of `{{` and `}}` by setting the new values in JavaScript or in templates. + +#### Setting in JavaScript + +The `Mustache.tags` property holds an array consisting of the opening and closing tag values. Set custom values by passing a new array of tags to `render()`, which gets honored over the default values, or by overriding the `Mustache.tags` property itself: + +```js +var customTags = [ '<%', '%>' ]; +``` + +##### Pass Value into Render Method +```js +Mustache.render(template, view, {}, customTags); +``` + +##### Override Tags Property +```js +Mustache.tags = customTags; +// Subsequent parse() and render() calls will use customTags +``` + +#### Setting in Templates + +Set Delimiter tags start with an equals sign and change the tag delimiters from `{{` and `}}` to custom strings. + +Consider the following contrived example: + +```html+erb +* {{ default_tags }} +{{=<% %>=}} +* <% erb_style_tags %> +<%={{ }}=%> +* {{ default_tags_again }} +``` + +Here we have a list with three items. The first item uses the default tag style, the second uses ERB style as defined by the Set Delimiter tag, and the third returns to the default style after yet another Set Delimiter declaration. + +According to [ctemplates](https://htmlpreview.github.io/?https://raw.githubusercontent.com/OlafvdSpek/ctemplate/master/doc/howto.html), this "is useful for languages like TeX, where double-braces may occur in the text and are awkward to use for markup." + +Custom delimiters may not contain whitespace or the equals sign. + +## Pre-parsing and Caching Templates + +By default, when mustache.js first parses a template it keeps the full parsed token tree in a cache. The next time it sees that same template it skips the parsing step and renders the template much more quickly. If you'd like, you can do this ahead of time using `mustache.parse`. + +```js +Mustache.parse(template); + +// Then, sometime later. +Mustache.render(template, view); +``` + +## Command line tool + +mustache.js is shipped with a Node.js based command line tool. It might be installed as a global tool on your computer to render a mustache template of some kind + +```bash +$ npm install -g mustache + +$ mustache dataView.json myTemplate.mustache > output.html +``` + +also supports stdin. + +```bash +$ cat dataView.json | mustache - myTemplate.mustache > output.html +``` + +or as a package.json `devDependency` in a build process maybe? + +```bash +$ npm install mustache --save-dev +``` + +```json +{ + "scripts": { + "build": "mustache dataView.json myTemplate.mustache > public/output.html" + } +} +``` +```bash +$ npm run build +``` + +The command line tool is basically a wrapper around `Mustache.render` so you get all the features. + +If your templates use partials you should pass paths to partials using `-p` flag: + +```bash +$ mustache -p path/to/partial1.mustache -p path/to/partial2.mustache dataView.json myTemplate.mustache +``` + +## Plugins for JavaScript Libraries + +mustache.js may be built specifically for several different client libraries, including the following: + + - [jQuery](http://jquery.com/) + - [MooTools](http://mootools.net/) + - [Dojo](http://www.dojotoolkit.org/) + - [YUI](http://developer.yahoo.com/yui/) + - [qooxdoo](http://qooxdoo.org/) + +These may be built using [Rake](http://rake.rubyforge.org/) and one of the following commands: +```bash +$ rake jquery +$ rake mootools +$ rake dojo +$ rake yui3 +$ rake qooxdoo +``` + +## TypeScript + +Since the source code of this package is written in JavaScript, we follow the [TypeScript publishing docs](https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html) preferred approach +by having type definitions available via [@types/mustache](https://www.npmjs.com/package/@types/mustache). + +## Testing + +In order to run the tests you'll need to install [Node.js](http://nodejs.org/). + +You also need to install the sub module containing [Mustache specifications](http://github.com/mustache/spec) in the project root. +```bash +$ git submodule init +$ git submodule update +``` +Install dependencies. +```bash +$ npm install +``` +Then run the tests. +```bash +$ npm test +``` +The test suite consists of both unit and integration tests. If a template isn't rendering correctly for you, you can make a test for it by doing the following: + + 1. Create a template file named `mytest.mustache` in the `test/_files` + directory. Replace `mytest` with the name of your test. + 2. Create a corresponding view file named `mytest.js` in the same directory. + This file should contain a JavaScript object literal enclosed in + parentheses. See any of the other view files for an example. + 3. Create a file with the expected output in `mytest.txt` in the same + directory. + +Then, you can run the test with: +```bash +$ TEST=mytest npm run test-render +``` + +### Browser tests + +Browser tests are not included in `npm test` as they run for too long, although they are ran automatically on Travis when merged into master. Run browser tests locally in any browser: +```bash +$ npm run test-browser-local +``` +then point your browser to `http://localhost:8080/__zuul` + +## Who uses mustache.js? + +An updated list of mustache.js users is kept [on the Github wiki](https://github.com/janl/mustache.js/wiki/Beard-Competition). Add yourself or your company if you use mustache.js! + +## Contributing + +mustache.js is a mature project, but it continues to actively invite maintainers. You can help out a high-profile project that is used in a lot of places on the web. No big commitment required, if all you do is review a single [Pull Request](https://github.com/janl/mustache.js/pulls), you are a maintainer. And a hero. + +### Your First Contribution + +- review a [Pull Request](https://github.com/janl/mustache.js/pulls) +- fix an [Issue](https://github.com/janl/mustache.js/issues) +- update the [documentation](https://github.com/janl/mustache.js#usage) +- make a website +- write a tutorial + +## Thanks + +mustache.js wouldn't kick ass if it weren't for these fine souls: + + * Chris Wanstrath / defunkt + * Alexander Lang / langalex + * Sebastian Cohnen / tisba + * J Chris Anderson / jchris + * Tom Robinson / tlrobinson + * Aaron Quint / quirkey + * Douglas Crockford + * Nikita Vasilyev / NV + * Elise Wood / glytch + * Damien Mathieu / dmathieu + * Jakub Kuźma / qoobaa + * Will Leinweber / will + * dpree + * Jason Smith / jhs + * Aaron Gibralter / agibralter + * Ross Boucher / boucher + * Matt Sanford / mzsanford + * Ben Cherry / bcherry + * Michael Jackson / mjackson + * Phillip Johnsen / phillipj + * David da Silva Contín / dasilvacontin diff --git a/orcinus/libcurlemu/README.txt b/orcinus/libcurlemu/README.txt new file mode 100644 index 0000000..70a78b3 --- /dev/null +++ b/orcinus/libcurlemu/README.txt @@ -0,0 +1,99 @@ +CURL Extension Emulation Library +Version 1.0.4 +Copyright 2004-2007, Steve Blinch +http://code.blitzaffe.com +============================================================================ + +DESCRIPTION + +Provides a pure-PHP implementation of the PHP CURL extension, for use on +systems which do not already have the CURL extension installed. It emulates +all of the curl_* functions normally provided by the CURL extension itself. + +This will automatically detect and use the best CURL implementation available +on your server. It will attempt the following, in order: + +1) Check for the existence of the "real" CURL PHP Extension. If it is +loaded, the library will do nothing (and it will not interfere with the +"real" extension). +2) Check for the existence of the CURL console binary (usually located in +/usr/bin/curl). If found, the library will emulate the CURL PHP +extension (including all curl_* functions) and use the console binary +to execute all requests. +3) If neither the "real" CURL PHP Extension nor the CURL console binary +are available, the library will emulate the CURL PHP extension (including +all curl_* functions) using a native, pure-PHP HTTP client implementation. +This implementation is somewhat limited, but it provides support for most +of the common CURL options. HTTPS (SSL) support is available in this +mode under PHP 4.3.0 if the OpenSSL Extension is loaded. + +Thus, by including this library in your project, you can rely on having some +level of CURL support regardless of the configuration of the server on which +it is being used. + + +HISTORY + +1.0.4 (not released) +- Fixed HTTPRetriever double-inclusion bug. + + +USAGE + +Simply copy all of the libcurlemu files into your project directory, then: + +require_once("libcurlemu.inc.php"); + +After this, you can use all of the curl_* functions documented in the PHP +Manual. + + +EXAMPLE + +// CURL Extension Emulation Library Example +// +// Usage should be straightforward; you simply use this script exactly as you +// would normally use the PHP CURL extension functions. + +// first, include libcurlemu.inc.php +require_once('libcurlemu.inc.php'); + +// at this point, libcurlemu has detected the best available CURL solution +// (either the CURL extension, if available, or the CURL commandline binary, +// if available, or as a last resort, HTTPRetriever, our native-PHP HTTP +// client implementation) and has implemented the curl_* functions if +// necessary, so you can use CURL normally and safely assume that all CURL +// functions are available. + +// the rest of this example code is copied straight from the PHP manual's +// reference for the curl_init() function, and will work fine with libcurlemu + +// create a new CURL resource +$ch = curl_init(); + +// set URL and other appropriate options +curl_setopt($ch, CURLOPT_URL, "http://www.example.com/"); +curl_setopt($ch, CURLOPT_HEADER, false); + +// grab URL and pass it to the browser +curl_exec($ch); + +// close CURL resource, and free up system resources +curl_close($ch); + + +LICENSE + +This script is free software; you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This script is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along +with this script; if not, write to the Free Software Foundation, Inc., +59 Temple Place, Suite 330, Boston, MA 02111-1307 USA diff --git a/orcinus/libcurlemu/class_HTTPRetriever.php b/orcinus/libcurlemu/class_HTTPRetriever.php new file mode 100644 index 0000000..3615599 --- /dev/null +++ b/orcinus/libcurlemu/class_HTTPRetriever.php @@ -0,0 +1,1162 @@ +curl_proxy (only useable when + * $http->force_curl is TRUE; internal support not yet implemented) + * + * + * 1.1.9 (11-Oct-2006) + * - Added set_transfer_display() and default_transfer_callback() + * methods for transfer progress tracking + * - Suppressed possible "fatal protocol error" when remote SSL server + * closes the connection early + * - Added get_content_type() method + * - make_query_string() now handles arrays + * + * 1.1.8 (19-Jun-2006) + * - Added set_progress_display() and default_progress_callback() + * methods for debug output + * - Added support for relative URLs in HTTP redirects + * - Added cookie support (sending and receiving) + * - Numerous bug fixes + * + * 1.1.7 (18-Apr-2006) + * - Added support for automatically following HTTP redirects + * - Added ::get_error() method to get any available error message (be + * it an HTTP result error or an internal/connection error) + * - Added ::cache_hit variable to determine whether the page was cached + * + * 1.1.6 (04-Mar-2006) + * - Added stream_timeout class variable. + * - Added progress_callback class variable. + * - Added support for braindead servers that ignore Connection: close + * + * + * EXAMPLE + * + * // HTTPRetriever usage example + * require_once("class_HTTPRetriever.php"); + * $http = &new HTTPRetriever(); + * + * + * // Example GET request: + * // ---------------------------------------------------------------------------- + * $keyword = "blitzaffe code"; // search Google for this keyword + * if (!$http->get("http://www.google.com/search?hl=en&q=%22".urlencode($keyword)."%22&btnG=Search&meta=")) { + * echo "HTTP request error: #{$http->result_code}: {$http->result_text}"; + * return false; + * } + * echo "HTTP response headers:
";
+ * var_dump($http->response_headers);
+ * echo "

"; + * + * echo "Page content:
";
+ * echo $http->response;
+ * echo "
"; + * // ---------------------------------------------------------------------------- + * + * + * // Example POST request: + * // ---------------------------------------------------------------------------- + * $keyword = "blitzaffe code"; // search Google for this keyword + * $values = array( + * "hl"=>"en", + * "q"=>"%22".urlencode($keyword)."%22", + * "btnG"=>"Search", + * "meta"=>"" + * ); + * // Note: This example is just to demonstrate the POST equivalent of the GET + * // example above; running this script will return a 501 Not Implemented, as + * // Google does not support POST requests. + * if (!$http->post("http://www.google.com/search",$http->make_query_string($values))) { + * echo "HTTP request error: #{$http->result_code}: {$http->result_text}"; + * return false; + * } + * echo "HTTP response headers:
";
+ * var_dump($http->response_headers);
+ * echo "

"; + * + * echo "Page content:
";
+ * echo $http->response;
+ * echo "
"; + * // ---------------------------------------------------------------------------- + * + * + * LICENSE + * + * This script is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This script is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along + * with this script; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +// define user agent ID's +define("UA_EXPLORER",0); +define("UA_MOZILLA",1); +define("UA_FIREFOX",2); +define("UA_OPERA",3); + +// define progress message severity levels +define('HRP_DEBUG',0); +define('HRP_INFO',1); +define('HRP_ERROR',2); + +if (!defined("CURL_PATH")) define("CURL_PATH","/usr/bin/curl"); + +// if the CURL extension is not loaded, but the CURL Emulation Library is found, try +// to load it +if (!extension_loaded("curl") && !defined('HTTPR_NO_REDECLARE_CURL') ) { + foreach (array(dirname(__FILE__)."/",dirname(__FILE__)."/libcurlemu/") as $k=>$libcurlemupath) { + $libcurlemuinc = $libcurlemupath.'libcurlemu.inc.php'; + if (is_readable($libcurlemuinc)) require_once($libcurlemuinc); + } +} + +class HTTPRetriever { + + // Constructor + function HTTPRetriever() { + // default HTTP headers to send with all requests + $this->headers = array( + "Referer"=>"", + "User-Agent"=>"HTTPRetriever/1.0", + "Connection"=>"close" + ); + + // HTTP version (has no effect if using CURL) + $this->version = "1.1"; + + // Normally, CURL is only used for HTTPS requests; setting this to + // TRUE will force CURL for HTTP requests as well. Not recommended. + $this->force_curl = false; + + // If you don't want to use CURL at all, set this to TRUE. + $this->disable_curl = false; + + // If HTTPS request return an error message about SSL certificates in + // $this->error and you don't care about security, set this to TRUE + $this->insecure_ssl = false; + + // Set the maximum time to wait for a connection + $this->connect_timeout = 15; + + // Set the maximum time to allow a transfer to run, or 0 to disable. + $this->max_time = 0; + + // Set the maximum time for a socket read/write operation, or 0 to disable. + $this->stream_timeout = 0; + + // If you're making an HTTPS request to a host whose SSL certificate + // doesn't match its domain name, AND YOU FULLY UNDERSTAND THE + // SECURITY IMPLICATIONS OF IGNORING THIS PROBLEM, set this to TRUE. + $this->ignore_ssl_hostname = false; + + // If TRUE, the get() and post() methods will close the connection + // and return immediately after receiving the HTTP result code + $this->result_close = false; + + // If set to a positive integer value, retrieved pages will be cached + // for this number of seconds. Any subsequent calls within the cache + // period will return the cached page, without contacting the remote + // server. + $this->caching = false; + + // If TRUE and $this->caching is not false, retrieved pages/files will be + // cached only if they appear to be static. + $this->caching_intelligent = false; + + // If TRUE, cached files will be stored in subdirectories corresponding + // to the first 2 letters of the hash filename + $this->caching_highvolume = false; + + // If $this->caching is enabled, this specifies the folder under which + // cached pages are saved. + $this->cache_path = '/tmp/'; + + // Set these to perform basic HTTP authentication + $this->auth_username = ''; + $this->auth_password = ''; + + // Optionally set this to a valid callback method to have HTTPRetriever + // provide page preprocessing capabilities to your script. If set, this + // method should accept two arguments: an object representing an instance + // of HTTPRetriever, and a string containing the page contents + $this->page_preprocessor = null; + + // Optionally set this to a valid callback method to have HTTPRetriever + // provide progress messages. Your callback must accept 2 parameters: + // an integer representing the severity (0=debug, 1=information, 2=error), + // and a string representing the progress message + $this->progress_callback = null; + + // Optionally set this to a valid callback method to have HTTPRetriever + // provide bytes-transferred messages. Your callbcak must accept 2 + // parameters: an integer representing the number of bytes transferred, + // and an integer representing the total number of bytes expected (or + // -1 if unknown). + $this->transfer_callback = null; + + // Set this to TRUE if you HTTPRetriever to transparently follow HTTP + // redirects (code 301, 302, 303, and 307). Optionally set this to a + // numeric value to limit the maximum number of redirects to the specified + // value. (Redirection loops are detected automatically.) + // Note that non-GET/HEAD requests will NOT be redirected except on code + // 303, as per HTTP standards. + $this->follow_redirects = false; + } + + // Send an HTTP GET request to $url; if $ipaddress is specified, the + // connection will be made to the selected IP instead of resolving the + // hostname in $url. + // + // If $cookies is set, it should be an array in one of two formats. + // + // Either: $cookies[ 'cookiename' ] = array ( + // '/path/'=>array( + // 'expires'=>time(), + // 'domain'=>'yourdomain.com', + // 'value'=>'cookievalue' + // ) + // ); + // + // Or, a more simplified format: + // $cookies[ 'cookiename' ] = 'value'; + // + // The former format will automatically check to make sure that the path, domain, + // and expiration values match the HTTP request, and will only send the cookie if + // they do match. The latter will force the cookie to be set for the HTTP request + // unconditionally. + // + function get($url,$ipaddress = false,$cookies = false) { + $this->method = "GET"; + $this->post_data = ""; + $this->connect_ip = $ipaddress; + return $this->_execute_request($url,$cookies); + } + + // Send an HTTP POST request to $url containing the POST data $data. See ::get() + // for a description of the remaining arguments. + function post($url,$data="",$ipaddress = false,$cookies = false) { + $this->method = "POST"; + $this->post_data = $data; + $this->connect_ip = $ipaddress; + return $this->_execute_request($url,$cookies); + } + + // Send an HTTP HEAD request to $url. See ::get() for a description of the arguments. + function head($url,$ipaddress = false,$cookies = false) { + $this->method = "HEAD"; + $this->post_data = ""; + $this->connect_ip = $ipaddress; + return $this->_execute_request($url,$cookies); + } + + // send an alternate (non-GET/POST) HTTP request to $url + function custom($method,$url,$data="",$ipaddress = false,$cookies = false) { + $this->method = $method; + $this->post_data = $data; + $this->connect_ip = $ipaddress; + return $this->_execute_request($url,$cookies); + } + + function array_to_query($arrayname,$arraycontents) { + $output = ""; + foreach ($arraycontents as $key=>$value) { + if (is_array($value)) { + $output .= $this->array_to_query(sprintf('%s[%s]',$arrayname,urlencode($key)),$value); + } else { + $output .= sprintf('%s[%s]=%s&',$arrayname,urlencode($key),urlencode($value)); + } + } + return $output; + } + + // builds a query string from the associative array array $data; + // returns a string that can be passed to $this->post() + function make_query_string($data) { + $output = ""; + if (is_array($data)) { + foreach ($data as $name=>$value) { + if (is_array($value)) { + $output .= $this->array_to_query(urlencode($name),$value); + } elseif (is_scalar($value)) { + $output .= urlencode($name)."=".urlencode($value)."&"; + } else { + $output .= urlencode($name)."=".urlencode(serialize($value)).'&'; + } + } + } + return substr($output,0,strlen($output)-1); + } + + + // this is pretty limited... but really, if you're going to spoof you UA, you'll probably + // want to use a Windows OS for the spoof anyway + // + // if you want to set the user agent to a custom string, just assign your string to + // $this->headers["User-Agent"] directly + function set_user_agent($agenttype,$agentversion,$windowsversion) { + $useragents = array( + "Mozilla/4.0 (compatible; MSIE %agent%; Windows NT %os%)", // IE + "Mozilla/5.0 (Windows; U; Windows NT %os%; en-US; rv:%agent%) Gecko/20040514", // Moz + "Mozilla/5.0 (Windows; U; Windows NT %os%; en-US; rv:1.7) Gecko/20040803 Firefox/%agent%", // FFox + "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT %os%) Opera %agent% [en]", // Opera + ); + $agent = $useragents[$agenttype]; + $this->headers["User-Agent"] = str_replace(array("%agent%","%os%"),array($agentversion,$windowsversion),$agent); + } + + // this isn't presently used as it's now handled inline by the request parser + function remove_chunkiness() { + $remaining = $this->response; + $this->response = ""; + + while ($remaining) { + $hexlen = strpos($remaining,"\r"); + $chunksize = substr($remaining,0,$hexlen); + $argstart = strpos($chunksize,';'); + if ($argstart!==false) $chunksize = substr($chunksize,0,$argstart); + $chunksize = (int) @hexdec($chunksize); + + $this->response .= substr($remaining,$hexlen+2,$chunksize); + $remaining = substr($remaining,$hexlen+2+$chunksize+2); + + if (!$chunksize) { + // either we're done, or something's borked... exit + $this->response .= $remaining; + return; + } + } + } + + // (internal) store a page in the cache + function _cache_store($token,$url) { + + if ($this->caching_intelligent) { + $urlinfo = parse_url($url); + if ($this->method=='POST') { + $this->progress(HRP_DEBUG,"POST request; not caching"); + return; + } else if (strlen($urlinfo['query'])) { + $this->progress(HRP_DEBUG,"Request used query string; not caching"); + return; + } else { + $this->progress(HRP_DEBUG,"Request appears to be static and cacheable"); + } + } + + $values = array( + "stats"=>$this->stats, + "result_code"=>$this->result_code, + "result_text"=>$this->result_text, + "version"=>$this->version, + "response"=>$this->response, + "response_headers"=>$this->response_headers, + "response_cookies"=>$this->response_cookies, + "raw_response"=>$this->raw_response, + ); + $values = serialize($values); + + $cache_dir = $this->cache_path; + if (substr($cache_dir,-1)!='/') $cache_dir .= '/'; + + if ($this->caching_highvolume) { + $cache_dir .= substr($token,0,2) . '/'; + if (!is_dir($cache_dir)) @mkdir($cache_dir); + } + + $filename = $cache_dir.$token.'.tmp'; + + $fp = @fopen($filename,"w"); + if (!$fp) { + $this->progress(HRP_DEBUG,"Unable to create cache file"); + return false; + } + fwrite($fp,$values); + fclose($fp); + + $this->progress(HRP_DEBUG,"HTTP response stored to cache"); + } + + // (internal) fetch a page from the cache + function _cache_fetch($token) { + $this->cache_hit = false; + $this->progress(HRP_DEBUG,"Checking for cached page value"); + + $cache_dir = $this->cache_path; + if (substr($cache_dir,-1)!='/') $cache_dir .= '/'; + + if ($this->caching_highvolume) $cache_dir .= substr($token,0,2) . '/'; + + $filename = $cache_dir.$token.'.tmp'; + if (!file_exists($filename)) { + $this->progress(HRP_DEBUG,"Page not available in cache"); + return false; + } + + if (time()-filemtime($filename)>$this->caching) { + $this->progress(HRP_DEBUG,"Page in cache is expired"); + @unlink($filename); + return false; + } + + if ($values = file_get_contents($filename)) { + $values = unserialize($values); + if (!$values) { + $this->progress(HRP_DEBUG,"Invalid cache contents"); + return false; + } + + $this->stats = $values["stats"]; + $this->result_code = $values["result_code"]; + $this->result_text = $values["result_text"]; + $this->version = $values["version"]; + $this->response = $values["response"]; + $this->response_headers = $values["response_headers"]; + $this->response_cookies = $values["response_cookies"]; + $this->raw_response = $values["raw_response"]; + + $this->progress(HRP_DEBUG,"Page loaded from cache"); + $this->cache_hit = true; + return true; + } else { + $this->progress(HRP_DEBUG,"Error reading cache file"); + return false; + } + } + + function parent_path($path) { + if (substr($path,0,1)=='/') $path = substr($path,1); + if (substr($path,-1)=='/') $path = substr($path,0,strlen($path)-1); + $path = explode('/',$path); + array_pop($path); + return count($path) ? ('/' . implode('/',$path)) : ''; + } + + // $cookies should be an array in one of two formats. + // + // Either: $cookies[ 'cookiename' ] = array ( + // '/path/'=>array( + // 'expires'=>time(), + // 'domain'=>'yourdomain.com', + // 'value'=>'cookievalue' + // ) + // ); + // + // Or, a more simplified format: + // $cookies[ 'cookiename' ] = 'value'; + // + // The former format will automatically check to make sure that the path, domain, + // and expiration values match the HTTP request, and will only send the cookie if + // they do match. The latter will force the cookie to be set for the HTTP request + // unconditionally. + // + function response_to_request_cookies($cookies,$urlinfo) { + + // check for simplified cookie format (name=value) + $cookiekeys = array_keys($cookies); + if (!count($cookiekeys)) return; + + $testkey = array_pop($cookiekeys); + if (!is_array($cookies[ $testkey ])) { + foreach ($cookies as $k=>$v) $this->request_cookies[$k] = $v; + return; + } + + // must not be simplified format, so parse as complex format: + foreach ($cookies as $name=>$paths) { + foreach ($paths as $path=>$values) { + // make sure the cookie isn't expired + if ( isset($values['expires']) && ($values['expires']request_cookies[$name] = $values['value']; + } + } + } + + // Execute the request for a particular URL, and transparently follow + // HTTP redirects if enabled. If $cookies is specified, it is assumed + // to be an array received from $this->response_cookies and will be + // processed to determine which cookies are valid for this host/URL. + function _execute_request($url,$cookies = false) { + // valid codes for which we transparently follow a redirect + $redirect_codes = array(301,302,303,307); + // valid methods for which we transparently follow a redirect + $redirect_methods = array('GET','HEAD'); + + $request_result = false; + + $this->followed_redirect = false; + $this->response_cookies = array(); + $this->cookie_headers = ''; + + $previous_redirects = array(); + do { + // send the request + $request_result = $this->_send_request($url,$cookies); + $lasturl = $url; + $url = false; + + // see if a redirect code was received + if ($this->follow_redirects && in_array($this->result_code,$redirect_codes)) { + + // only redirect on a code 303 or if the method was GET/HEAD + if ( ($this->result_code==303) || in_array($this->method,$redirect_methods) ) { + + // parse the information from the OLD URL so that we can handle + // relative links + $oldurlinfo = parse_url($lasturl); + + $url = $this->response_headers['Location']; + + // parse the information in the new URL, and fill in any blanks + // using values from the old URL + $urlinfo = parse_url($url); + foreach ($oldurlinfo as $k=>$v) { + if (!$urlinfo[$k]) $urlinfo[$k] = $v; + } + + // create an absolute path + if (substr($urlinfo['path'],0,1)!='/') { + $baseurl = $oldurlinfo['path']; + if (substr($baseurl,-1)!='/') $baseurl = $this->parent_path($url) . '/'; + $urlinfo['path'] = $baseurl . $urlinfo['path']; + } + + // rebuild the URL + $url = $this->rebuild_url($urlinfo); + + $this->method = "GET"; + $this->post_data = ""; + + $this->progress(HRP_INFO,'Redirected to '.$url); + } + } + + if ( $url && strlen($url) ) { + + if (isset($previous_redirects[$url])) { + $this->error = "Infinite redirection loop"; + $request_result = false; + break; + } + if ( is_numeric($this->follow_redirects) && (count($previous_redirects)>$this->follow_redirects) ) { + $this->error = "Exceeded redirection limit"; + $request_result = false; + break; + } + + $previous_redirects[$url] = true; + } + + } while ($url && strlen($url)); + + // clear headers that shouldn't persist across multiple requests + $per_request_headers = array('Host','Content-Length'); + foreach ($per_request_headers as $k=>$v) unset($this->headers[$v]); + + if (count($previous_redirects)>1) $this->followed_redirect = array_keys($previous_redirects); + + return $request_result; + } + + // private - sends an HTTP request to $url + function _send_request($url,$cookies = false) { + $this->progress(HRP_INFO,"Initiating {$this->method} request for $url"); + if ($this->caching) { + $cachetoken = md5($url.'|'.$this->post_data); + if ($this->_cache_fetch($cachetoken)) return true; + } + + $time_request_start = $this->getmicrotime(); + + $urldata = parse_url($url); + $this->urldata = &$urldata; + $http_host = $urldata['host'] . (isset($urldata['port']) ? ':'.$urldata['port'] : ''); + + if (!isset($urldata["port"]) || !$urldata["port"]) $urldata["port"] = ($urldata["scheme"]=="https") ? 443 : 80; + if (!isset($urldata["path"]) || !$urldata["path"]) $urldata["path"] = '/'; + + if (!empty($urldata['user'])) $this->auth_username = $urldata['user']; + if (!empty($urldata['pass'])) $this->auth_password = $urldata['pass']; + + //echo "Sending HTTP/{$this->version} {$this->method} request for ".$urldata["host"].":".$urldata["port"]." page ".$urldata["path"]."
"; + + if ($this->version>"1.0") $this->headers["Host"] = $http_host; + if ($this->method=="POST") { + $this->headers["Content-Length"] = strlen($this->post_data); + if (!isset($this->headers["Content-Type"])) $this->headers["Content-Type"] = "application/x-www-form-urlencoded"; + } + + if ( !empty($this->auth_username) || !empty($this->auth_password) ) { + $this->headers['Authorization'] = 'Basic '.base64_encode($this->auth_username.':'.$this->auth_password); + } else { + unset($this->headers['Authorization']); + } + + if (is_array($cookies)) { + $this->response_to_request_cookies($cookies,$urldata); + } + + if (!empty($urldata["query"])) $urldata["path"] .= "?".$urldata["query"]; + $request = $this->method." ".$urldata["path"]." HTTP/".$this->version."\r\n"; + $request .= $this->build_headers(); + $request .= $this->post_data; + + $this->response = ""; + + // clear headers that shouldn't persist across multiple requests + // (we can do this here as we've already built the request, including headers, above) + $per_request_headers = array('Host','Content-Length'); + foreach ($per_request_headers as $k=>$v) unset($this->headers[$v]); + + // Native SSL support requires the OpenSSL extension, and was introduced in PHP 4.3.0 + $php_ssl_support = extension_loaded("openssl") && version_compare(phpversion(),"4.3.0")>=0; + + // if this is a plain HTTP request, or if it's an HTTPS request and OpenSSL support is available, + // natively perform the HTTP request + if ( ( ($urldata["scheme"]=="http") || ($php_ssl_support && ($urldata["scheme"]=="https")) ) && (!$this->force_curl) ) { + $curl_mode = false; + + $hostname = $this->connect_ip ? $this->connect_ip : $urldata['host']; + if ($urldata["scheme"]=="https") $hostname = 'ssl://'.$hostname; + + $time_connect_start = $this->getmicrotime(); + + $this->progress(HRP_INFO,'Opening socket connection to '.$hostname.' port '.$urldata['port']); + + $this->expected_bytes = -1; + $this->received_bytes = 0; + + $fp = @fsockopen ($hostname,$urldata["port"],$errno,$errstr,$this->connect_timeout); + $time_connected = $this->getmicrotime(); + $connect_time = $time_connected - $time_connect_start; + if ($fp) { + if ($this->stream_timeout) stream_set_timeout($fp,$this->stream_timeout); + $this->progress(HRP_INFO,"Connected; sending request"); + + $this->progress(HRP_DEBUG,$request); + fputs ($fp, $request); + $this->raw_request = $request; + + if ($this->stream_timeout) { + $meta = socket_get_status($fp); + if ($meta['timed_out']) { + $this->error = "Exceeded socket write timeout of ".$this->stream_timeout." seconds"; + $this->progress(HRP_ERROR,$this->error); + return false; + } + } + + $this->progress(HRP_INFO,"Request sent; awaiting reply"); + + $headers_received = false; + $data_length = false; + $chunked = false; + $iterations = 0; + while (!feof($fp)) { + if ($data_length>0) { + $line = fread($fp,$data_length); + $this->progress(HRP_DEBUG,"[DL] Got a line: [{$line}] " . gettype($line)); + + if ($line!==false) $data_length -= strlen($line); + } else { + $line = @fgets($fp,10240); + $this->progress(HRP_DEBUG,"[NDL] Got a line: [{$line}] " . gettype($line)); + + if ( ($chunked) && ($line!==false) ) { + $line = trim($line); + if (!strlen($line)) continue; + + list($data_length,) = explode(';',$line,2); + $data_length = (int) hexdec(trim($data_length)); + + if ($data_length==0) { + $this->progress(HRP_DEBUG,"Done"); + // end of chunked data + break; + } + $this->progress(HRP_DEBUG,"Chunk length $data_length (0x$line)"); + continue; + } + } + + if ($line===false) { + $meta = socket_get_status($fp); + if ($meta['timed_out']) { + if ($this->stream_timeout) { + $this->error = "Exceeded socket read timeout of ".$this->stream_timeout." seconds"; + } else { + $this->error = "Exceeded default socket read timeout"; + } + $this->progress(HRP_ERROR,$this->error); + return false; + } else { + $this->progress(HRP_ERROR,'No data but not timed out'); + } + continue; + } + + // check time limits if requested + if ($this->max_time>0) { + if ($this->getmicrotime() - $time_request_start > $this->max_time) { + $this->error = "Exceeded maximum transfer time of ".$this->max_time." seconds"; + $this->progress(HRP_ERROR,$this->error); + return false; + break; + } + } + + $this->response .= $line; + + $iterations++; + if ($headers_received) { + if ($time_connected>0) { + $time_firstdata = $this->getmicrotime(); + $process_time = $time_firstdata - $time_connected; + $time_connected = 0; + } + $this->received_bytes += strlen($line); + if ($iterations % 20 == 0) { + $this->update_transfer_counters(); + } + } + + + // some dumbass webservers don't respect Connection: close and just + // leave the connection open, so we have to be diligent about + // calculating the content length so we can disconnect at the end of + // the response + if ( (!$headers_received) && (trim($line)=="") ) { + $headers_received = true; + $this->progress(HRP_DEBUG,"Got headers: {$this->response}"); + + if (preg_match('/^Content-Length: ([0-9]+)/im',$this->response,$matches)) { + $data_length = (int) $matches[1]; + $this->progress(HRP_DEBUG,"Content length is $data_length"); + $this->expected_bytes = $data_length; + $this->update_transfer_counters(); + } else { + $this->progress(HRP_DEBUG,"No data length specified"); + } + if (preg_match("/^Transfer-Encoding: chunked/im",$this->response,$matches)) { + $chunked = true; + $this->progress(HRP_DEBUG,"Chunked transfer encoding requested"); + } else { + $this->progress(HRP_DEBUG,"CTE not requested"); + } + + if (preg_match_all("/^Set-Cookie: ((.*?)\=(.*?)(?:;\s*(.*))?)$/im",$this->response,$cookielist,PREG_SET_ORDER)) { + foreach ($cookielist as $k=>$cookie) $this->cookie_headers .= $cookie[0]."\n"; + + // get the path for which cookies will be valid if no path is specified + $cookiepath = preg_replace('/\/{2,}/','',$urldata['path']); + if (substr($cookiepath,-1)!='/') { + $cookiepath = explode('/',$cookiepath); + array_pop($cookiepath); + $cookiepath = implode('/',$cookiepath) . '/'; + } + // process each cookie + foreach ($cookielist as $k=>$cookiedata) { + list(,$rawcookie,$name,$value,$attributedata) = $cookiedata; + $attributedata = explode(';',trim($attributedata)); + $attributes = array(); + + $cookie = array( + 'value'=>$value, + 'raw'=>trim($rawcookie), + ); + foreach ($attributedata as $k=>$attribute) { + list($attrname,$attrvalue) = explode('=',trim($attribute)); + $cookie[$attrname] = $attrvalue; + } + + if (!isset($cookie['domain']) || !$cookie['domain']) $cookie['domain'] = $urldata['host']; + if (!isset($cookie['path']) || !$cookie['path']) $cookie['path'] = $cookiepath; + if (isset($cookie['expires']) && $cookie['expires']) $cookie['expires'] = strtotime($cookie['expires']); + + if (!$this->validate_response_cookie($cookie,$urldata['host'])) continue; + + // do not store expired cookies; if one exists, unset it + if ( isset($cookie['expires']) && ($cookie['expires']response_cookies[ $name ][ $cookie['path'] ]); + continue; + } + + $this->response_cookies[ $name ][ $cookie['path'] ] = $cookie; + } + } + } + + if ($this->result_close) { + if (preg_match_all("/HTTP\/([0-9\.]+) ([0-9]+) (.*?)[\r\n]/",$this->response,$matches)) { + $resultcodes = $matches[2]; + foreach ($resultcodes as $k=>$code) { + if ($code!=100) { + $this->progress(HRP_INFO,'HTTP result code received; closing connection'); + + $this->result_code = $code; + $this->result_text = $matches[3][$k]; + fclose($fp); + + return ($this->result_code==200); + } + } + } + } + } + if (feof($fp)) $this->progress(HRP_DEBUG,'EOF on socket'); + @fclose ($fp); + + $this->update_transfer_counters(); + + if (is_array($this->response_cookies)) { + // make sure paths are sorted in the order in which they should be applied + // when setting response cookies + foreach ($this->response_cookies as $name=>$paths) { + ksort($this->response_cookies[$name]); + } + } + $this->progress(HRP_INFO,'Request complete'); + } else { + $this->error = strtoupper($urldata["scheme"])." connection to ".$hostname." port ".$urldata["port"]." failed"; + $this->progress(HRP_ERROR,$this->error); + return false; + } + + // perform an HTTP/HTTPS request using CURL + } elseif ( !$this->disable_curl && ( ($urldata["scheme"]=="https") || ($this->force_curl) ) ) { + $this->progress(HRP_INFO,'Passing HTTP request for $url to CURL'); + $curl_mode = true; + if (!$this->_curl_request($url)) return false; + + // unknown protocol + } else { + $this->error = "Unsupported protocol: ".$urldata["scheme"]; + $this->progress(HRP_ERROR,$this->error); + return false; + } + + $this->raw_response = $this->response; + + $totallength = strlen($this->response); + + do { + $headerlength = strpos($this->response,"\r\n\r\n"); + + $response_headers = explode("\r\n",substr($this->response,0,$headerlength)); + $http_status = trim(array_shift($response_headers)); + foreach ($response_headers as $line) { + list($k,$v) = explode(":",$line,2); + $this->response_headers[trim($k)] = trim($v); + } + $this->response = substr($this->response,$headerlength+4); + + /* // Handled in-transfer now + if (($this->response_headers['Transfer-Encoding']=="chunked") && (!$curl_mode)) { + $this->remove_chunkiness(); + } + */ + + if (!preg_match("/^HTTP\/([0-9\.]+) ([0-9]+) (.*?)$/",$http_status,$matches)) { + $matches = array("",$this->version,0,"HTTP request error"); + } + list (,$response_version,$this->result_code,$this->result_text) = $matches; + + // skip HTTP result code 100 (Continue) responses + } while (($this->result_code==100) && ($headerlength)); + + // record some statistics, roughly compatible with CURL's curl_getinfo() + if (!$curl_mode) { + $total_time = $this->getmicrotime() - $time_request_start; + $transfer_time = $total_time - $connect_time; + $this->stats = array( + "total_time"=>$total_time, + "connect_time"=>$connect_time, // time between connection request and connection established + "process_time"=>$process_time, // time between HTTP request and first data (non-headers) received + "url"=>$url, + "content_type"=>$this->response_headers["Content-Type"], + "http_code"=>$this->result_code, + "header_size"=>$headerlength, + "request_size"=>$totallength, + "filetime"=>strtotime($this->response_headers["Date"]), + "pretransfer_time"=>$connect_time, + "size_download"=>$totallength, + "speed_download"=>$transfer_time > 0 ? round($totallength / $transfer_time) : 0, + "download_content_length"=>$totallength, + "upload_content_length"=>0, + "starttransfer_time"=>$connect_time, + ); + } + + + $ok = ($this->result_code==200); + if ($ok) { + // if a page preprocessor is defined, call it to process the page contents + if (is_callable($this->page_preprocessor)) $this->response = call_user_func($this->page_preprocessor,$this,$this->response); + + // if caching is enabled, save the page + if ($this->caching) $this->_cache_store($cachetoken,$url); + } + + return $ok; + } + + function validate_response_cookie($cookie,$actual_hostname) { + // make sure the cookie can't be set for a TLD, eg: '.com' + $cookiehost = $cookie['domain']; + $p = strrpos($cookiehost,'.'); + if ($p===false) return false; + + $tld = strtolower(substr($cookiehost,$p+1)); + $special_domains = array("com", "edu", "net", "org", "gov", "mil", "int"); + $periods_required = in_array($tld,$special_domains) ? 1 : 2; + + $periods = substr_count($cookiehost,'.'); + if ($periods<$periods_required) return false; + + if (substr($actual_hostname,0,1)!='.') $actual_hostname = '.'.$actual_hostname; + if (substr($cookiehost,0,1)!='.') $cookiehost = '.'.$cookiehost; + $domain_match = ( + ($actual_hostname==$cookiehost) || + (substr($actual_hostname,-strlen($cookiehost))==$cookiehost) + ); + + return $domain_match; + + } + + function build_headers() { + $headers = ""; + foreach ($this->headers as $name=>$value) { + $value = trim($value); + if (empty($value)) continue; + $headers .= "{$name}: {$value}\r\n"; + } + + if (isset($this->request_cookies) && is_array($this->request_cookies)) { + $cookielist = array(); + foreach ($this->request_cookies as $name=>$value) { + $cookielist[] = "{$name}={$value}"; + } + if (count($cookielist)) $headers .= "Cookie: ".implode('; ',$cookielist)."\r\n"; + } + + + $headers .= "\r\n"; + + return $headers; + } + + // opposite of parse_url() + function rebuild_url($urlinfo) { + $url = $urlinfo['scheme'].'://'; + + if ($urlinfo['user'] || $urlinfo['pass']) { + $url .= $urlinfo['user']; + if ($urlinfo['pass']) { + if ($urlinfo['user']) $url .= ':'; + $url .= $urlinfo['pass']; + } + $url .= '@'; + } + + $url .= $urlinfo['host']; + if ($urlinfo['port']) $url .= ':'.$urlinfo['port']; + + $url .= $urlinfo['path']; + + if ($urlinfo['query']) $url .= '?'.$urlinfo['query']; + if ($urlinfo['fragment']) $url .= '#'.$urlinfo['fragment']; + + return $url; + } + + function _replace_hostname(&$url,$new_hostname) { + $parts = parse_url($url); + $old_hostname = $parts['host']; + + $parts['host'] = $new_hostname; + + $url = $this->rebuild_url($parts); + + return $old_hostname; + } + + function _curl_request($url) { + $this->error = false; + + // if a direct connection IP address was specified, replace the hostname + // in the URL with the IP address, and set the Host: header to the + // original hostname + if ($this->connect_ip) { + $old_hostname = $this->_replace_hostname($url,$this->connect_ip); + $this->headers["Host"] = $old_hostname; + } + + + unset($this->headers["Content-Length"]); + $headers = explode("\n",$this->build_headers()); + + $ch = curl_init(); + curl_setopt($ch,CURLOPT_URL, $url); + curl_setopt($ch,CURLOPT_USERAGENT, $this->headers["User-Agent"]); + curl_setopt($ch,CURLOPT_HEADER, 1); + curl_setopt($ch,CURLOPT_RETURNTRANSFER, 1); +// curl_setopt($ch,CURLOPT_FOLLOWLOCATION, 1); // native method doesn't support this yet, so it's disabled for consistency + curl_setopt($ch,CURLOPT_TIMEOUT, 10); + if ($this->curl_proxy) { + curl_setopt($ch,CURLOPT_PROXY,$this->curl_proxy); + } + curl_setopt($ch,CURLOPT_HTTPHEADER, $headers); + + if ($this->method=="POST") { + curl_setopt($ch,CURLOPT_POST,1); + curl_setopt($ch,CURLOPT_POSTFIELDS,$this->post_data); + } + if ($this->insecure_ssl) { + curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,0); + } + if ($this->ignore_ssl_hostname) { + curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,1); + } + + $this->response = curl_exec ($ch); + if (curl_errno($ch)!=0) { + $this->error = "CURL error #".curl_errno($ch).": ".curl_error($ch); + } + + $this->stats = curl_getinfo($ch); + curl_close($ch); + + return ($this->error === false); + } + + function progress($level,$msg) { + if (is_callable($this->progress_callback)) call_user_func($this->progress_callback,$level,$msg); + } + + // Gets any available HTTPRetriever error message (including both internal + // errors and HTTP errors) + function get_error() { + return $this->error ? $this->error : 'HTTP ' . $this->result_code.': '.$this->result_text; + } + + function get_content_type() { + if (!$ctype = $this->response_headers['Content-Type']) { + $ctype = $this->response_headers['Content-type']; + } + list($ctype,) = explode(';',$ctype); + + return strtolower($ctype); + } + + function update_transfer_counters() { + if (is_callable($this->transfer_callback)) call_user_func($this->transfer_callback,$this->received_bytes,$this->expected_bytes); + } + + function set_transfer_display($enabled = true) { + if ($enabled) { + $this->transfer_callback = array(&$this,'default_transfer_callback'); + } else { + unset($this->transfer_callback); + } + } + + function set_progress_display($enabled = true) { + if ($enabled) { + $this->progress_callback = array(&$this,'default_progress_callback'); + } else { + unset($this->progress_callback); + } + } + + function default_progress_callback($severity,$message) { + $severities = array( + HRP_DEBUG=>'debug', + HRP_INFO=>'info', + HRP_ERROR=>'error', + ); + + echo date('Y-m-d H:i:sa').' ['.$severities[$severity].'] '.$message."\n"; + flush(); + } + + function default_transfer_callback($transferred,$expected) { + $msg = "Transferred " . round($transferred/1024,1); + if ($expected>=0) $msg .= "/" . round($expected/1024,1); + $msg .= "KB"; + if ($expected>0) $msg .= " (".round($transferred*100/$expected,1)."%)"; + echo date('Y-m-d H:i:sa')." $msg\n"; + flush(); + } + + function getmicrotime() { + list($usec, $sec) = explode(" ",microtime()); + return ((float)$usec + (float)$sec); + } +} +?> \ No newline at end of file diff --git a/orcinus/libcurlemu/example.php b/orcinus/libcurlemu/example.php new file mode 100644 index 0000000..b953cf9 --- /dev/null +++ b/orcinus/libcurlemu/example.php @@ -0,0 +1,35 @@ + diff --git a/orcinus/libcurlemu/libcurlemu.inc.php b/orcinus/libcurlemu/libcurlemu.inc.php new file mode 100644 index 0000000..a42932c --- /dev/null +++ b/orcinus/libcurlemu/libcurlemu.inc.php @@ -0,0 +1,111 @@ + \ No newline at end of file diff --git a/orcinus/libcurlemu/libcurlexternal.inc.php b/orcinus/libcurlemu/libcurlexternal.inc.php new file mode 100644 index 0000000..4338a29 --- /dev/null +++ b/orcinus/libcurlemu/libcurlexternal.inc.php @@ -0,0 +1,664 @@ +$v) { + if ( (substr($k,0,8)=="CURLOPT_") && ($v==$curlopt)) return $k; + } + return false; +} + +// Initialize a CURL emulation session +function curl_init($url=false) { + $i = $GLOBALS["_CURLEXT_OPT"]["index"]++; + $GLOBALS["_CURLEXT_OPT"][$i] = array("url"=>$url); + + return $i; +} + +// Set an option for a CURL emulation transfer +function curl_setopt($ch,$option,$value) { + + $opt = &$GLOBALS["_CURLEXT_OPT"][$ch]; + if (!$opt["args"]) $opt["args"] = array(); + $args = &$opt["args"]; + if (!$opt["settings"]) $opt["settings"] = array(); + $settings = &$opt["settings"]; + + switch($option) { + case CURLOPT_URL: + $opt["url"] = $value; + break; + case CURLOPT_VERBOSE: + $opt["verbose"] = $value>0; + break; + case CURLOPT_USERPWD: + if ($value==="") $value = false; + $settings["user"] = $value; + break; + case CURLOPT_PROXYUSERPWD: + if ($value==="") $value = false; + $settings["proxy-user"] = $value; + break; + case CURLOPT_COOKIE: + if ($value==="") $value = false; + if ( is_bool($value) || (strpos($value,"=")!==false) ) $settings["cookie"] = $value; + break; + case CURLOPT_COOKIEFILE: + if ($value==="") $value = false; + $settings["cookie"] = $value; + break; + case CURLOPT_COOKIEJAR: + if ($value==="") $value = false; + $settings["cookie-jar"] = $value; + break; + case CURLOPT_CUSTOMREQUEST: + if ($value==="") $value = false; + $settings["request"] = $value; + break; + case CURLOPT_PROXY: + if ($value==="") $value = false; + $settings["proxy"] = $value; + break; + case CURLOPT_INTERFACE: + if ($value==="") $value = false; + $settings["interface"] = $value; + break; + case CURLOPT_KRB4LEVEL: + if ($value==="") $value = false; + $settings["krb4"] = $value; + break; + case CURLOPT_SSLCERT: + $pass = ""; + if (is_string($settings["cert"])) { + list(,$pass) = explode(":",$settings["cert"]); + if (strlen($pass)) $pass = ":$pass"; + } + $settings["cert"] = $value.$pass; + break; + case CURLOPT_SSLCERTPASSWD: + $filename = ""; + if (is_string($settings["cert"])) { + list($filename,) = explode(":",$settings["cert"]); + } + $settings["cert"] = $filename.":".$value; + break; + case CURLOPT_RANGE: + if ($value==="") $value = false; + $settings["range"] = $value; + break; + case CURLOPT_REFERER: + if ($value==="") $value = false; + $settings["referer"] = $value; + break; + case CURLOPT_NOBODY: + $settings["head"] = $value>0; + break; + case CURLOPT_FAILONERROR: + $opt["fail_on_error"] = $value>0; + break; + case CURLOPT_USERAGENT: + $settings["user-agent"] = $value; + break; + case CURLOPT_HEADER: + $settings["include"] = $value>0; + break; + case CURLOPT_RETURNTRANSFER: + $opt["return_transfer"] = $value>0; + break; + case CURLOPT_TIMEOUT: + $settings["max-time"] = (int) $value; + break; + case CURLOPT_HTTPHEADER: + reset($value); + foreach ($value as $k=>$header) $args[] = "header=".$header; + break; + case CURLOPT_POST: + $settings["data"]["enabled"] = $value>0; + break; + case CURLOPT_POSTFIELDS: + if ($value==="") $value = false; + $settings["data"]["value"] = $value; + break; + case CURLOPT_SSL_VERIFYPEER: + $settings["insecure"] = !$value; + break; + case CURLOPT_HTTP_VERSION: + switch ($value){ + case 1: + $settings["http1.0"] = true; + break; + case 2: + $settings["http1.1"] = true; + break; + case 3: + $settings["http2"] = true; + break; + } + + break; + case CURLOPT_SSL_VERIFYHOST: + // not supported by the commandline client + break; + case CURLOPT_FOLLOWLOCATION: + $settings["location"] = $value>0; + break; + case CURLOPT_PUT: + $settings["upload-file"]["enabled"] = $value>0; + break; + case CURLOPT_INFILE: + if ($value==="") $value = false; + + if (is_resource($value)) { + + // Ugh, this is a terrible hack. The CURL extension accepts a file handle, but + // the CURL binary obviously wants a filename. Since you can't derive a filename + // from a file handle, we have to make a copy of the file from the file handle, + // then pass the temporary filename to the CURL binary. + + $tmpfilename = tempnam("/tmp","cif"); + $fp = @fopen($tmpfilename,"w"); + if (!$fp) { + trigger_error("CURL emulation library could not create a temporary file for CURLOPT_INFILE; upload aborted",E_USER_WARNING); + } else { + while (!feof($value)) { + $contents = fread($value,8192); + fwrite($fp,$contents); + } + fclose($fp); + // if a temporary file was previously created, unlink it + if ($settings["upload-file"]["value"] && file_exists($settings["upload-file"]["value"])) unlink($settings["upload-file"]["value"]); + + // set the new upload-file filename + $settings["upload-file"]["value"] = $tmpfilename; + } + } else { + trigger_error("CURLOPT_INFILE must specify a valid file resource",E_USER_WARNING); + } + + break; + case CURLOPT_MUTE: + // we're already mute, no? + break; + case CURLOPT_LOW_SPEED_LIMIT: + $settings["speed-limit"] = (int) $value; + break; + case CURLOPT_LOW_SPEED_TIME: + $settings["speed-time"] = (int) $value; + break; + case CURLOPT_RESUME_FROM: + $settings["continue-at"] = (int) $value; + break; + case CURLOPT_CAINFO: + if ($value==="") $value = false; + $settings["cacert"] = $value; + break; + case CURLOPT_SSLVERSION: + $value = (int) $value; + switch($value) { + case 2: + case 3: + unset($settings["sslv2"]); + unset($settings["sslv3"]); + $settings["sslv".$value] = true; + break; + } + break; + case CURLOPT_TIMECONDITION: + // untested - I'm lazy :) + if (!isset($settings["time-cond"]["enabled"])) $settings["time-cond"]["enabled"] = false; + if (!$settings["time-cond"]["value"]) $settings["time-cond"]["value"] = 1; + + $settings["time-cond"]["value"] = abs($settings["time-cond"]["value"]); + if ($value==TIMECOND_ISUNMODSINCE) { + $settings["time-cond"]["value"] *= -1; + } + + break; + case CURLOPT_TIMEVALUE: + // untested - I'm lazy :) + if ($settings["time-cond"]["value"]) { + $sign = $settings["time-cond"]["value"] / abs($settings["time-cond"]["value"]); + } else { + $sign = 1; + } + $settings["time-cond"]["value"] = (int) $value * $sign; + break; + case CURLOPT_FILE: + if (is_resource($value)) { + $opt["output_handle"] = $value; + } else { + trigger_error("CURLOPT_FILE must specify a valid file resource",E_USER_WARNING); + } + break; + case CURLOPT_WRITEHEADER: + if (is_resource($value)) { + $opt["header_handle"] = $value; + } else { + trigger_error("CURLOPT_WRITEHEADER must specify a valid file resource",E_USER_WARNING); + } + break; + case CURLOPT_HEADERFUNCTION: + $opt["header_function"] = $value; + break; + case CURLOPT_STDERR: + // not implemented for now - not really relevant + break; + case CURLOPT_CONNECTTIMEOUT: + $opt["connect-timeout"] = $value; + break; + // FTP stuff not implemented + case CURLOPT_QUOTE: + case CURLOPT_POSTQUOTE: + case CURLOPT_UPLOAD: + case CURLOPT_FTPLISTONLY: + case CURLOPT_FTPAPPEND: + case CURLOPT_FTPPORT: + // Other stuff not implemented + case CURLOPT_NETRC: + default: + trigger_error("CURL emulation does not implement CURL option "._curlopt_name($option),E_USER_WARNING); + break; + } +} + +// Perform a CURL emulation session +function curl_exec($ch) { + $opt = &$GLOBALS["_CURLEXT_OPT"][$ch]; + $url = $opt["url"]; + $verbose = $opt["verbose"]; + + // ask commandline CURL to return its statistics at the end of its output + $opt["settings"]["write-out"] = "\n%{http_code}|%{time_total}|%{time_namelookup}|%{time_connect}|%{time_pretransfer}|%{time_starttransfer}|%{size_download}|%{size_upload}|%{size_header}|%{size_request}|%{speed_download}|%{speed_upload}|||||||%{content_type}|%{url_effective}"; + $writeout_order = array( + CURLINFO_HTTP_CODE, + CURLINFO_TOTAL_TIME, + CURLINFO_NAMELOOKUP_TIME, + CURLINFO_CONNECT_TIME, + CURLINFO_PRETRANSFER_TIME, + CURLINFO_STARTTRANSFER_TIME, + CURLINFO_SIZE_DOWNLOAD, + CURLINFO_SIZE_UPLOAD, + CURLINFO_HEADER_SIZE, + CURLINFO_REQUEST_SIZE, + CURLINFO_SPEED_DOWNLOAD, + CURLINFO_SPEED_UPLOAD, + + // the following 5 items are not provided by commandline CURL, and thus are left empty + CURLINFO_FILETIME, + CURLINFO_REDIRECT_TIME, + CURLINFO_SSL_VERIFYRESULT, + CURLINFO_CONTENT_LENGTH_DOWNLOAD, + CURLINFO_CONTENT_LENGTH_UPLOAD, + CURLINFO_REDIRECT_COUNT, + + CURLINFO_CONTENT_TYPE, + CURLINFO_EFFECTIVE_URL, + ); + + // if the CURLOPT_NOBODY option was specified (to remove the body from the output), + // but an output file handle was set, we need to tell CURL to return the body so + // that we can write it to the output handle and strip it from the output + if ($opt["settings"]["head"] && $opt["output_handle"]) { + unset($opt["settings"]["head"]); + $strip_body = true; + } + // if the CURLOPT_HEADER option was NOT specified, but a header file handle was + // specified, we again need to tell CURL to return the headers so we can write + // them, then strip them from the output + if (!isset($opt["settings"]["include"]) && (isset($opt["header_handle"]) || isset($opt["header_function"]))) { + $opt["settings"]["include"] = true; + $strip_headers = true; + } + + // build the CURL argument list + $arguments = ""; + foreach ($opt["args"] as $k=>$arg) { + list($argname,$argval) = explode('=',$arg,2); + $arguments .= "--$argname ".escapeshellarg($argval)." "; + } + foreach ($opt["settings"] as $argname=>$argval) { + if (is_array($argval)) { + if (isset($argval["enabled"]) && !$argval["enabled"]) continue; + $argval = $argval["value"]; + } + if ($argval===false) continue; + $arguments .= "--$argname ".(is_bool($argval)?"":escapeshellarg($argval)." "); + } + + // build the CURL commandline and execute it + $cmd = CURL_PATH." ".$arguments." ".escapeshellarg($url); + + if ($verbose) echo "libcurlemu: Executing: $cmd\n"; + exec($cmd,$output,$ret); + + if ($verbose) { + echo "libcurlemu: Result: "; + var_dump($output); + echo "libcurlemu: Exit code: $ret\n"; + } + + // check for errors + $opt["errno"] = $ret; + if ($ret) $opt["error"] = "CURL error #$ret"; + + // die if CURLOPT_FAILONERROR is set and the HTTP result code is greater than 300 + if ($opt["fail_on_error"]) { + if (preg_match("/^HTTP\/1.[0-9]+ ([0-9]{3}) /",$output[0],$matches)) { + $resultcode = (int) $matches[1]; + if ($resultcode>300) die; + } else { + die; // couldn't get result code! + } + } + + // pull the statistics out from the output + $stats = explode('|',array_pop($output)); + foreach ($writeout_order as $k=>$item) { + $opt["stats"][$item] = $stats[$k]; + } + + // build the response string + $output = implode("\r\n",$output); + + + // find the header end position if needed + if ($strip_headers || $strip_body || isset($opt["header_handle"]) || isset($opt["header_function"])) { + $headerpos = strpos($output,"\r\n\r\n"); + while(preg_match("/HTTP\/1.[0-9]+ [0-9]{3} /",substr($output,$headerpos+4))){ + $headerpos = strpos($output,"\r\n\r\n",$headerpos+4); + } + } + + // if a file handle was provided for header output, extract the headers + // and write them to the handle + if (isset($opt["header_handle"])) { + $headers = substr($output,0,$headerpos); + fwrite($opt["header_handle"],$headers); + } + + if (isset($opt["header_function"])) { + $headers = substr($output,0,$headerpos); + call_user_func($opt["header_function"],$ch,$headers); + } + + // if the caller did not request headers in the output, strip them + if ($strip_headers) { + $output = substr($output,$headerpos+4); + } + + // if the caller did not request the response body in the output, strip it + if ($strip_body) { + if ($strip_headers) { + $body = $output; + $output = ""; + } else { + $body = substr($output,$headerpos+4); + $output = substr($output,0,$headerpos); + } + } + + // if a file handle was provided for output, write the output to it + if (isset($opt["output_handle"])) { + fwrite($opt["output_handle"],$output); + + // if the caller requested that the response be returned, return it + } elseif ($opt["return_transfer"]) { + return $output; + + // otherwise, just echo the output to stdout + } else { + echo $output; + } + return true; +} + +function curl_close($ch) { + $opt = &$GLOBALS["_CURLEXT_OPT"][$ch]; + + if ($opt["settings"]) { + $settings = &$opt["settings"]; + // if the user used CURLOPT_INFILE to specify a file to upload, remove the + // temporary file created for the CURL binary + if ($settings["upload-file"]["value"] && file_exists($settings["upload-file"]["value"])) unlink($settings["upload-file"]["value"]); + } + + unset($GLOBALS["_CURLEXT_OPT"][$ch]); +} + +function curl_errno($ch) { + return (int) $GLOBALS["_CURLEXT_OPT"][$ch]["errno"]; +} + +function curl_error($ch) { + return $GLOBALS["_CURLEXT_OPT"][$ch]["error"]; +} + +function curl_getinfo($ch,$opt=NULL) { + if ($opt) { + return $GLOBALS["_CURLEXT_OPT"][$ch]["stats"][$opt]; + } else { + $curlinfo_tags = array( + "url"=>CURLINFO_EFFECTIVE_URL, + "content_type"=>CURLINFO_CONTENT_TYPE, + "http_code"=>CURLINFO_HTTP_CODE, + "header_size"=>CURLINFO_HEADER_SIZE, + "request_size"=>CURLINFO_REQUEST_SIZE, + "filetime"=>CURLINFO_FILETIME, + "ssl_verify_result"=>CURLINFO_SSL_VERIFYRESULT, + "redirect_count"=>CURLINFO_REDIRECT_COUNT, + "total_time"=>CURLINFO_TOTAL_TIME, + "namelookup_time"=>CURLINFO_NAMELOOKUP_TIME, + "connect_time"=>CURLINFO_CONNECT_TIME, + "pretransfer_time"=>CURLINFO_PRETRANSFER_TIME, + "size_upload"=>CURLINFO_SIZE_UPLOAD, + "size_download"=>CURLINFO_SIZE_DOWNLOAD, + "speed_download"=>CURLINFO_SPEED_DOWNLOAD, + "speed_upload"=>CURLINFO_SPEED_UPLOAD, + "download_content_length"=>CURLINFO_CONTENT_LENGTH_DOWNLOAD, + "upload_content_length"=>CURLINFO_CONTENT_LENGTH_UPLOAD, + "starttransfer_time"=>CURLINFO_STARTTRANSFER_TIME, + "redirect_time"=>CURLINFO_REDIRECT_TIME + ); + $res = array(); + foreach ($curlinfo_tags as $tag=>$opt) { + $res[$tag] = $GLOBALS["_CURLEXT_OPT"][$ch]["stats"][$opt]; + } + return $res; + } +} + +function curl_version() { + return "libcurlemu/".CURLEXT_VERSION."-ext"; +} + +} +?> \ No newline at end of file diff --git a/orcinus/libcurlemu/libcurlnative.inc.php b/orcinus/libcurlemu/libcurlnative.inc.php new file mode 100644 index 0000000..6dd629c --- /dev/null +++ b/orcinus/libcurlemu/libcurlnative.inc.php @@ -0,0 +1,453 @@ +$v) { + if ( (substr($k,0,8)=="CURLOPT_") && ($v==$curlopt)) return $k; + } + return false; +} + +// Initialize a CURL emulation session +function curl_init() { + $i = $GLOBALS["_CURLNAT_OPT"]["index"]++; + $GLOBALS["_CURLNAT_OPT"][$i] = array(); + $GLOBALS["_CURLNAT_OPT"][$i]["http"] = &new HTTPRetriever(); + $GLOBALS["_CURLNAT_OPT"][$i]["include_body"] = true; + return $i; +} + +// Set an option for a CURL emulation transfer +function curl_setopt($ch,$option,$value) { + + $opt = &$GLOBALS["_CURLNAT_OPT"][$ch]; + if (!$opt["args"]) $opt["args"] = array(); + $args = &$opt["args"]; + if (!$opt["settings"]) $opt["settings"] = array(); + $settings = &$opt["settings"]; + $http = &$opt["http"]; + + switch($option) { + case CURLOPT_URL: + $opt["url"] = $value; + break; + case CURLOPT_CUSTOMREQUEST: + $opt["method"] = $value; + break; + case CURLOPT_REFERER: + $http->headers["Referer"] = $value; + break; + case CURLOPT_NOBODY: + $opt["include_body"] = $value==0; + break; + case CURLOPT_FAILONERROR: + $opt["fail_on_error"] = $value>0; + break; + case CURLOPT_USERAGENT: + $http->headers["User-Agent"] = $value; + break; + case CURLOPT_HEADER: + $opt["include_headers"] = $value>0; + break; + case CURLOPT_RETURNTRANSFER: + $opt["return_transfer"] = $value>0; + break; + case CURLOPT_TIMEOUT: + $opt["max-time"] = (int) $value; + break; + case CURLOPT_HTTPHEADER: + reset($value); + foreach ($value as $k=>$header) { + list($headername,$headervalue) = explode(":",$header); + $http->headers[$headername] = ltrim($headervalue); + } + break; + case CURLOPT_POST: + $opt["post"] = $value>0; + break; + case CURLOPT_POSTFIELDS: + $opt["postdata"] = $value; + break; + case CURLOPT_MUTE: + // we're already mute, no? + break; + case CURLOPT_FILE: + if (is_resource($value)) { + $opt["output_handle"] = $value; + } else { + trigger_error("CURLOPT_FILE must specify a valid file resource",E_USER_WARNING); + } + break; + case CURLOPT_WRITEHEADER: + if (is_resource($value)) { + $opt["header_handle"] = $value; + } else { + trigger_error("CURLOPT_WRITEHEADER must specify a valid file resource",E_USER_WARNING); + } + break; + case CURLOPT_STDERR: + // not implemented for now - not really relevant + break; + + case CURLOPT_SSL_VERIFYPEER: + case CURLOPT_SSL_VERIFYHOST: + // these are automatically disabled using ssl:// anyway + break; + + case CURLOPT_USERPWD: + list($curl_user,$curl_pass) = explode(':',$value,2); + $http->auth_username = $curl_user; + $http->auth_password = $curl_pass; + break; + + // Important stuff not implemented (as it's not yet supported by HTTPRetriever) + case CURLOPT_PUT: + case CURLOPT_INFILE: + case CURLOPT_FOLLOWLOCATION: + case CURLOPT_PROXYUSERPWD: + case CURLOPT_COOKIE: + case CURLOPT_COOKIEFILE: + case CURLOPT_PROXY: + case CURLOPT_RANGE: + case CURLOPT_RESUME_FROM: + + // Things that cannot (reasonably) be implemented here + case CURLOPT_LOW_SPEED_LIMIT: + case CURLOPT_LOW_SPEED_TIME: + case CURLOPT_KRB4LEVEL: + case CURLOPT_SSLCERT: + case CURLOPT_SSLCERTPASSWD: + case CURLOPT_SSLVERSION: + case CURLOPT_INTERFACE: + case CURLOPT_CAINFO: + case CURLOPT_TIMECONDITION: + case CURLOPT_TIMEVALUE: + + // FTP stuff not implemented + case CURLOPT_QUOTE: + case CURLOPT_POSTQUOTE: + case CURLOPT_UPLOAD: + case CURLOPT_FTPLISTONLY: + case CURLOPT_FTPAPPEND: + case CURLOPT_FTPPORT: + + // Other stuff not implemented + case CURLOPT_VERBOSE: + case CURLOPT_NETRC: + default: + trigger_error("CURL emulation does not implement CURL option "._curlopt_name($option),E_USER_WARNING); + break; + } +} + +// Perform a CURL emulation session +function curl_exec($ch) { + $opt = &$GLOBALS["_CURLNAT_OPT"][$ch]; + $url = $opt["url"]; + + $http = &$opt["http"]; + $http->disable_curl = true; // avoid problems with recursion, since we *ARE* CURL + + // set time limits if requested + if ($opt["max-time"]) { + $http->connect_timeout = $opt["max-time"]; + $http->max_time = $opt["max-time"]; + } + + if ($opt["post"]) { + $res = $http->post($url,$opt["postdata"]); + } elseif ($opt["method"]) { + $res = $http->custom($opt["method"],$url,$opt["postdata"]); + } else { + $res = $http->get($url); + } + + // check for errors + $opt["errno"] = (!$res && $http->error) ? 1 : 0; + if ($opt["errno"]) $opt["error"] = $http->error; + + // die if CURLOPT_FAILONERROR is set and the HTTP result code is greater than 300 + if ($opt["fail_on_error"]) { + if ($http->result_code>300) die; + } + + $opt["stats"] = $http->stats; + + + $headers = ""; + foreach ($http->response_headers as $k=>$v) { + $headers .= "$k: $v\r\n"; + } + + // if a file handle was provided for header output, extract the headers + // and write them to the handle + if (isset($opt["header_handle"])) { + fwrite($opt["header_handle"],$headers); + } + + $output = ($opt["include_headers"] ? $headers."\r\n" : "") . ($opt["include_body"] ? $http->response : ""); + + // if a file handle was provided for output, write the output to it + if (isset($opt["output_handle"])) { + fwrite($opt["output_handle"],$output); + + // if the caller requested that the response be returned, return it + } elseif ($opt["return_transfer"]) { + return $output; + + // otherwise, just echo the output to stdout + } else { + echo $output; + } + return true; +} + +function curl_close($ch) { + $opt = &$GLOBALS["_CURLNAT_OPT"][$ch]; + + if ($opt["settings"]) { + $settings = &$opt["settings"]; + // if the user used CURLOPT_INFILE to specify a file to upload, remove the + // temporary file created for the CURL binary + if ($settings["upload-file"]["value"] && file_exists($settings["upload-file"]["value"])) unlink($settings["upload-file"]["value"]); + } + + unset($GLOBALS["_CURLNAT_OPT"][$ch]); +} + +function curl_errno($ch) { + return (int) $GLOBALS["_CURLNAT_OPT"][$ch]["errno"]; +} + +function curl_error($ch) { + return $GLOBALS["_CURLNAT_OPT"][$ch]["error"]; +} + +function curl_getinfo($ch,$opt=NULL) { + if ($opt) { + $curlinfo_tags = array( + CURLINFO_EFFECTIVE_URL=>"url", + CURLINFO_CONTENT_TYPE=>"content_type", + CURLINFO_HTTP_CODE=>"http_code", + CURLINFO_HEADER_SIZE=>"header_size", + CURLINFO_REQUEST_SIZE=>"request_size", + CURLINFO_FILETIME=>"filetime", + CURLINFO_SSL_VERIFYRESULT=>"ssl_verify_result", + CURLINFO_REDIRECT_COUNT=>"redirect_count", + CURLINFO_TOTAL_TIME=>"total_time", + CURLINFO_NAMELOOKUP_TIME=>"namelookup_time", + CURLINFO_CONNECT_TIME=>"connect_time", + CURLINFO_PRETRANSFER_TIME=>"pretransfer_time", + CURLINFO_SIZE_UPLOAD=>"size_upload", + CURLINFO_SIZE_DOWNLOAD=>"size_download", + CURLINFO_SPEED_DOWNLOAD=>"speed_download", + CURLINFO_SPEED_UPLOAD=>"speed_upload", + CURLINFO_CONTENT_LENGTH_DOWNLOAD=>"download_content_length", + CURLINFO_CONTENT_LENGTH_UPLOAD=>"upload_content_length", + CURLINFO_STARTTRANSFER_TIME=>"starttransfer_time", + CURLINFO_REDIRECT_TIME=>"redirect_time" + ); + + $key = $curlinfo_tags[$opt]; + return $GLOBALS["_CURLNAT_OPT"][$ch]["stats"][$key]; + } else { + return $GLOBALS["_CURLNAT_OPT"][$ch]["stats"]; + } +} + +function curl_version() { + return "libcurlemu/".CURLNAT_VERSION."-nat"; +} + +} +?> \ No newline at end of file diff --git a/orcinus/mustache/.gitattributes b/orcinus/mustache/.gitattributes new file mode 100644 index 0000000..95b7ab4 --- /dev/null +++ b/orcinus/mustache/.gitattributes @@ -0,0 +1,8 @@ +/test export-ignore +/CONTRIBUTING.md export-ignore +/.php_cs export-ignore +/phpunit.xml.dist export-ignore +/.travis.yml export-ignore +/.styleci.yml export-ignore +/.gitmodules export-ignore +/.gitignore export-ignore diff --git a/orcinus/mustache/LICENSE b/orcinus/mustache/LICENSE new file mode 100644 index 0000000..e0aecc9 --- /dev/null +++ b/orcinus/mustache/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2010-2015 Justin Hileman + +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/orcinus/mustache/README.md b/orcinus/mustache/README.md new file mode 100644 index 0000000..1560192 --- /dev/null +++ b/orcinus/mustache/README.md @@ -0,0 +1,73 @@ +Mustache.php +============ + +A [Mustache](https://mustache.github.io/) implementation in PHP. + +[![Package version](http://img.shields.io/packagist/v/mustache/mustache.svg?style=flat-square)](https://packagist.org/packages/mustache/mustache) +[![Build status](http://img.shields.io/travis/bobthecow/mustache.php/dev.svg?style=flat-square)](http://travis-ci.org/bobthecow/mustache.php) +[![StyleCI](https://styleci.io/repos/569670/shield)](https://styleci.io/repos/569670) +[![Monthly downloads](http://img.shields.io/packagist/dm/mustache/mustache.svg?style=flat-square)](https://packagist.org/packages/mustache/mustache) + + +Usage +----- + +A quick example: + +```php + ENT_QUOTES)); +echo $m->render('Hello {{planet}}', array('planet' => 'World!')); // "Hello World!" +``` + + +And a more in-depth example -- this is the canonical Mustache template: + +```html+jinja +Hello {{name}} +You have just won {{value}} dollars! +{{#in_ca}} +Well, {{taxed_value}} dollars, after taxes. +{{/in_ca}} +``` + + +Create a view "context" object -- which could also be an associative array, but those don't do functions quite as well: + +```php +value - ($this->value * 0.4); + } + + public $in_ca = true; +} +``` + + +And render it: + +```php + ENT_QUOTES)); +$chris = new Chris; +echo $m->render($template, $chris); +``` + +*Note:* we recommend using `ENT_QUOTES` as a default of [entity_flags](https://github.com/bobthecow/mustache.php/wiki#entity_flags) to decrease the chance of Cross-site scripting vulnerability. + +And That's Not All! +------------------- + +Read [the Mustache.php documentation](https://github.com/bobthecow/mustache.php/wiki/Home) for more information. + + +See Also +-------- + + * [mustache(5)](http://mustache.github.io/mustache.5.html) man page. + * [Readme for the Ruby Mustache implementation](http://github.com/defunkt/mustache/blob/master/README.md). diff --git a/orcinus/mustache/bin/build_bootstrap.php b/orcinus/mustache/bin/build_bootstrap.php new file mode 100644 index 0000000..cc23b2b --- /dev/null +++ b/orcinus/mustache/bin/build_bootstrap.php @@ -0,0 +1,178 @@ +#!/usr/bin/env php + + */ +class SymfonyClassCollectionLoader +{ + private static $loaded; + + const HEADER = <<<'EOS' +\s*$/'), '', file_get_contents($r->getFileName())); + } + + $cache = $cacheDir . '/' . $name . $extension; + $header = sprintf(self::HEADER, strftime('%Y')); + self::writeCacheFile($cache, $header . substr(self::stripComments('=5.2.4" + }, + "require-dev": { + "phpunit/phpunit": "~3.7|~4.0|~5.0", + "friendsofphp/php-cs-fixer": "~1.11" + }, + "autoload": { + "psr-0": { "Mustache": "src/" } + } +} diff --git a/orcinus/mustache/src/Mustache/Autoloader.php b/orcinus/mustache/src/Mustache/Autoloader.php new file mode 100644 index 0000000..e8ea3f4 --- /dev/null +++ b/orcinus/mustache/src/Mustache/Autoloader.php @@ -0,0 +1,88 @@ +baseDir = $realDir; + } else { + $this->baseDir = $baseDir; + } + } + + /** + * Register a new instance as an SPL autoloader. + * + * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..') + * + * @return Mustache_Autoloader Registered Autoloader instance + */ + public static function register($baseDir = null) + { + $key = $baseDir ? $baseDir : 0; + + if (!isset(self::$instances[$key])) { + self::$instances[$key] = new self($baseDir); + } + + $loader = self::$instances[$key]; + spl_autoload_register(array($loader, 'autoload')); + + return $loader; + } + + /** + * Autoload Mustache classes. + * + * @param string $class + */ + public function autoload($class) + { + if ($class[0] === '\\') { + $class = substr($class, 1); + } + + if (strpos($class, 'Mustache') !== 0) { + return; + } + + $file = sprintf('%s/%s.php', $this->baseDir, str_replace('_', '/', $class)); + if (is_file($file)) { + require $file; + } + } +} diff --git a/orcinus/mustache/src/Mustache/Cache.php b/orcinus/mustache/src/Mustache/Cache.php new file mode 100644 index 0000000..3292efa --- /dev/null +++ b/orcinus/mustache/src/Mustache/Cache.php @@ -0,0 +1,43 @@ +logger; + } + + /** + * Set a logger instance. + * + * @param Mustache_Logger|Psr\Log\LoggerInterface $logger + */ + public function setLogger($logger = null) + { + if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) { + throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.'); + } + + $this->logger = $logger; + } + + /** + * Add a log record if logging is enabled. + * + * @param string $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + protected function log($level, $message, array $context = array()) + { + if (isset($this->logger)) { + $this->logger->log($level, $message, $context); + } + } +} diff --git a/orcinus/mustache/src/Mustache/Cache/FilesystemCache.php b/orcinus/mustache/src/Mustache/Cache/FilesystemCache.php new file mode 100644 index 0000000..3e742b7 --- /dev/null +++ b/orcinus/mustache/src/Mustache/Cache/FilesystemCache.php @@ -0,0 +1,161 @@ +cache($className, $compiledSource); + * + * The FilesystemCache benefits from any opcode caching that may be setup in your environment. So do that, k? + */ +class Mustache_Cache_FilesystemCache extends Mustache_Cache_AbstractCache +{ + private $baseDir; + private $fileMode; + + /** + * Filesystem cache constructor. + * + * @param string $baseDir Directory for compiled templates + * @param int $fileMode Override default permissions for cache files. Defaults to using the system-defined umask + */ + public function __construct($baseDir, $fileMode = null) + { + $this->baseDir = $baseDir; + $this->fileMode = $fileMode; + } + + /** + * Load the class from cache using `require_once`. + * + * @param string $key + * + * @return bool + */ + public function load($key) + { + $fileName = $this->getCacheFilename($key); + if (!is_file($fileName)) { + return false; + } + + require_once $fileName; + + return true; + } + + /** + * Cache and load the compiled class. + * + * @param string $key + * @param string $value + */ + public function cache($key, $value) + { + $fileName = $this->getCacheFilename($key); + + $this->log( + Mustache_Logger::DEBUG, + 'Writing to template cache: "{fileName}"', + array('fileName' => $fileName) + ); + + $this->writeFile($fileName, $value); + $this->load($key); + } + + /** + * Build the cache filename. + * Subclasses should override for custom cache directory structures. + * + * @param string $name + * + * @return string + */ + protected function getCacheFilename($name) + { + return sprintf('%s/%s.php', $this->baseDir, $name); + } + + /** + * Create cache directory. + * + * @throws Mustache_Exception_RuntimeException If unable to create directory + * + * @param string $fileName + * + * @return string + */ + private function buildDirectoryForFilename($fileName) + { + $dirName = dirname($fileName); + if (!is_dir($dirName)) { + $this->log( + Mustache_Logger::INFO, + 'Creating Mustache template cache directory: "{dirName}"', + array('dirName' => $dirName) + ); + + @mkdir($dirName, 0777, true); + // @codeCoverageIgnoreStart + if (!is_dir($dirName)) { + throw new Mustache_Exception_RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName)); + } + // @codeCoverageIgnoreEnd + } + + return $dirName; + } + + /** + * Write cache file. + * + * @throws Mustache_Exception_RuntimeException If unable to write file + * + * @param string $fileName + * @param string $value + */ + private function writeFile($fileName, $value) + { + $dirName = $this->buildDirectoryForFilename($fileName); + + $this->log( + Mustache_Logger::DEBUG, + 'Caching compiled template to "{fileName}"', + array('fileName' => $fileName) + ); + + $tempFile = tempnam($dirName, basename($fileName)); + if (false !== @file_put_contents($tempFile, $value)) { + if (@rename($tempFile, $fileName)) { + $mode = isset($this->fileMode) ? $this->fileMode : (0666 & ~umask()); + @chmod($fileName, $mode); + + return; + } + + // @codeCoverageIgnoreStart + $this->log( + Mustache_Logger::ERROR, + 'Unable to rename Mustache temp cache file: "{tempName}" -> "{fileName}"', + array('tempName' => $tempFile, 'fileName' => $fileName) + ); + // @codeCoverageIgnoreEnd + } + + // @codeCoverageIgnoreStart + throw new Mustache_Exception_RuntimeException(sprintf('Failed to write cache file "%s".', $fileName)); + // @codeCoverageIgnoreEnd + } +} diff --git a/orcinus/mustache/src/Mustache/Cache/NoopCache.php b/orcinus/mustache/src/Mustache/Cache/NoopCache.php new file mode 100644 index 0000000..ed9eec9 --- /dev/null +++ b/orcinus/mustache/src/Mustache/Cache/NoopCache.php @@ -0,0 +1,47 @@ +log( + Mustache_Logger::WARNING, + 'Template cache disabled, evaluating "{className}" class at runtime', + array('className' => $key) + ); + eval('?>' . $value); + } +} diff --git a/orcinus/mustache/src/Mustache/Compiler.php b/orcinus/mustache/src/Mustache/Compiler.php new file mode 100644 index 0000000..93a295a --- /dev/null +++ b/orcinus/mustache/src/Mustache/Compiler.php @@ -0,0 +1,689 @@ +pragmas = $this->defaultPragmas; + $this->sections = array(); + $this->blocks = array(); + $this->source = $source; + $this->indentNextLine = true; + $this->customEscape = $customEscape; + $this->entityFlags = $entityFlags; + $this->charset = $charset; + $this->strictCallables = $strictCallables; + + return $this->writeCode($tree, $name); + } + + /** + * Enable pragmas across all templates, regardless of the presence of pragma + * tags in the individual templates. + * + * @internal Users should set global pragmas in Mustache_Engine, not here :) + * + * @param string[] $pragmas + */ + public function setPragmas(array $pragmas) + { + $this->pragmas = array(); + foreach ($pragmas as $pragma) { + $this->pragmas[$pragma] = true; + } + $this->defaultPragmas = $this->pragmas; + } + + /** + * Helper function for walking the Mustache token parse tree. + * + * @throws Mustache_Exception_SyntaxException upon encountering unknown token types + * + * @param array $tree Parse tree of Mustache tokens + * @param int $level (default: 0) + * + * @return string Generated PHP source code + */ + private function walk(array $tree, $level = 0) + { + $code = ''; + $level++; + foreach ($tree as $node) { + switch ($node[Mustache_Tokenizer::TYPE]) { + case Mustache_Tokenizer::T_PRAGMA: + $this->pragmas[$node[Mustache_Tokenizer::NAME]] = true; + break; + + case Mustache_Tokenizer::T_SECTION: + $code .= $this->section( + $node[Mustache_Tokenizer::NODES], + $node[Mustache_Tokenizer::NAME], + isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(), + $node[Mustache_Tokenizer::INDEX], + $node[Mustache_Tokenizer::END], + $node[Mustache_Tokenizer::OTAG], + $node[Mustache_Tokenizer::CTAG], + $level + ); + break; + + case Mustache_Tokenizer::T_INVERTED: + $code .= $this->invertedSection( + $node[Mustache_Tokenizer::NODES], + $node[Mustache_Tokenizer::NAME], + isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(), + $level + ); + break; + + case Mustache_Tokenizer::T_PARTIAL: + $code .= $this->partial( + $node[Mustache_Tokenizer::NAME], + isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '', + $level + ); + break; + + case Mustache_Tokenizer::T_PARENT: + $code .= $this->parent( + $node[Mustache_Tokenizer::NAME], + isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '', + $node[Mustache_Tokenizer::NODES], + $level + ); + break; + + case Mustache_Tokenizer::T_BLOCK_ARG: + $code .= $this->blockArg( + $node[Mustache_Tokenizer::NODES], + $node[Mustache_Tokenizer::NAME], + $node[Mustache_Tokenizer::INDEX], + $node[Mustache_Tokenizer::END], + $node[Mustache_Tokenizer::OTAG], + $node[Mustache_Tokenizer::CTAG], + $level + ); + break; + + case Mustache_Tokenizer::T_BLOCK_VAR: + $code .= $this->blockVar( + $node[Mustache_Tokenizer::NODES], + $node[Mustache_Tokenizer::NAME], + $node[Mustache_Tokenizer::INDEX], + $node[Mustache_Tokenizer::END], + $node[Mustache_Tokenizer::OTAG], + $node[Mustache_Tokenizer::CTAG], + $level + ); + break; + + case Mustache_Tokenizer::T_COMMENT: + break; + + case Mustache_Tokenizer::T_ESCAPED: + case Mustache_Tokenizer::T_UNESCAPED: + case Mustache_Tokenizer::T_UNESCAPED_2: + $code .= $this->variable( + $node[Mustache_Tokenizer::NAME], + isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(), + $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_ESCAPED, + $level + ); + break; + + case Mustache_Tokenizer::T_TEXT: + $code .= $this->text($node[Mustache_Tokenizer::VALUE], $level); + break; + + default: + throw new Mustache_Exception_SyntaxException(sprintf('Unknown token type: %s', $node[Mustache_Tokenizer::TYPE]), $node); + } + } + + return $code; + } + + const KLASS = 'lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context); + $buffer = \'\'; + %s + + return $buffer; + } + %s + %s + }'; + + const KLASS_NO_LAMBDAS = 'walk($tree); + $sections = implode("\n", $this->sections); + $blocks = implode("\n", $this->blocks); + $klass = empty($this->sections) && empty($this->blocks) ? self::KLASS_NO_LAMBDAS : self::KLASS; + + $callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : ''; + + return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections, $blocks); + } + + const BLOCK_VAR = ' + $blockFunction = $context->findInBlock(%s); + if (is_callable($blockFunction)) { + $buffer .= call_user_func($blockFunction, $context); + %s} + '; + + const BLOCK_VAR_ELSE = '} else {%s'; + + /** + * Generate Mustache Template inheritance block variable PHP source. + * + * @param array $nodes Array of child tokens + * @param string $id Section name + * @param int $start Section start offset + * @param int $end Section end offset + * @param string $otag Current Mustache opening tag + * @param string $ctag Current Mustache closing tag + * @param int $level + * + * @return string Generated PHP source code + */ + private function blockVar($nodes, $id, $start, $end, $otag, $ctag, $level) + { + $id = var_export($id, true); + + $else = $this->walk($nodes, $level); + if ($else !== '') { + $else = sprintf($this->prepare(self::BLOCK_VAR_ELSE, $level + 1, false, true), $else); + } + + return sprintf($this->prepare(self::BLOCK_VAR, $level), $id, $else); + } + + const BLOCK_ARG = '%s => array($this, \'block%s\'),'; + + /** + * Generate Mustache Template inheritance block argument PHP source. + * + * @param array $nodes Array of child tokens + * @param string $id Section name + * @param int $start Section start offset + * @param int $end Section end offset + * @param string $otag Current Mustache opening tag + * @param string $ctag Current Mustache closing tag + * @param int $level + * + * @return string Generated PHP source code + */ + private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level) + { + $key = $this->block($nodes); + $id = var_export($id, true); + + return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key); + } + + const BLOCK_FUNCTION = ' + public function block%s($context) + { + $indent = $buffer = \'\';%s + + return $buffer; + } + '; + + /** + * Generate Mustache Template inheritance block function PHP source. + * + * @param array $nodes Array of child tokens + * + * @return string key of new block function + */ + private function block($nodes) + { + $code = $this->walk($nodes, 0); + $key = ucfirst(md5($code)); + + if (!isset($this->blocks[$key])) { + $this->blocks[$key] = sprintf($this->prepare(self::BLOCK_FUNCTION, 0), $key, $code); + } + + return $key; + } + + const SECTION_CALL = ' + $value = $context->%s(%s);%s + $buffer .= $this->section%s($context, $indent, $value); + '; + + const SECTION = ' + private function section%s(Mustache_Context $context, $indent, $value) + { + $buffer = \'\'; + + if (%s) { + $source = %s; + $result = (string) call_user_func($value, $source, %s); + if (strpos($result, \'{{\') === false) { + $buffer .= $result; + } else { + $buffer .= $this->mustache + ->loadLambda($result%s) + ->renderInternal($context); + } + } elseif (!empty($value)) { + $values = $this->isIterable($value) ? $value : array($value); + foreach ($values as $value) { + $context->push($value); + %s + $context->pop(); + } + } + + return $buffer; + } + '; + + /** + * Generate Mustache Template section PHP source. + * + * @param array $nodes Array of child tokens + * @param string $id Section name + * @param string[] $filters Array of filters + * @param int $start Section start offset + * @param int $end Section end offset + * @param string $otag Current Mustache opening tag + * @param string $ctag Current Mustache closing tag + * @param int $level + * + * @return string Generated section PHP source code + */ + private function section($nodes, $id, $filters, $start, $end, $otag, $ctag, $level) + { + $source = var_export(substr($this->source, $start, $end - $start), true); + $callable = $this->getCallable(); + + if ($otag !== '{{' || $ctag !== '}}') { + $delimTag = var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true); + $helper = sprintf('$this->lambdaHelper->withDelimiters(%s)', $delimTag); + $delims = ', ' . $delimTag; + } else { + $helper = '$this->lambdaHelper'; + $delims = ''; + } + + $key = ucfirst(md5($delims . "\n" . $source)); + + if (!isset($this->sections[$key])) { + $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $delims, $this->walk($nodes, 2)); + } + + $method = $this->getFindMethod($id); + $id = var_export($id, true); + $filters = $this->getFilters($filters, $level); + + return sprintf($this->prepare(self::SECTION_CALL, $level), $method, $id, $filters, $key); + } + + const INVERTED_SECTION = ' + $value = $context->%s(%s);%s + if (empty($value)) { + %s + } + '; + + /** + * Generate Mustache Template inverted section PHP source. + * + * @param array $nodes Array of child tokens + * @param string $id Section name + * @param string[] $filters Array of filters + * @param int $level + * + * @return string Generated inverted section PHP source code + */ + private function invertedSection($nodes, $id, $filters, $level) + { + $method = $this->getFindMethod($id); + $id = var_export($id, true); + $filters = $this->getFilters($filters, $level); + + return sprintf($this->prepare(self::INVERTED_SECTION, $level), $method, $id, $filters, $this->walk($nodes, $level)); + } + + const PARTIAL_INDENT = ', $indent . %s'; + const PARTIAL = ' + if ($partial = $this->mustache->loadPartial(%s)) { + $buffer .= $partial->renderInternal($context%s); + } + '; + + /** + * Generate Mustache Template partial call PHP source. + * + * @param string $id Partial name + * @param string $indent Whitespace indent to apply to partial + * @param int $level + * + * @return string Generated partial call PHP source code + */ + private function partial($id, $indent, $level) + { + if ($indent !== '') { + $indentParam = sprintf(self::PARTIAL_INDENT, var_export($indent, true)); + } else { + $indentParam = ''; + } + + return sprintf( + $this->prepare(self::PARTIAL, $level), + var_export($id, true), + $indentParam + ); + } + + const PARENT = ' + if ($parent = $this->mustache->loadPartial(%s)) { + $context->pushBlockContext(array(%s + )); + $buffer .= $parent->renderInternal($context, $indent); + $context->popBlockContext(); + } + '; + + const PARENT_NO_CONTEXT = ' + if ($parent = $this->mustache->loadPartial(%s)) { + $buffer .= $parent->renderInternal($context, $indent); + } + '; + + /** + * Generate Mustache Template inheritance parent call PHP source. + * + * @param string $id Parent tag name + * @param string $indent Whitespace indent to apply to parent + * @param array $children Child nodes + * @param int $level + * + * @return string Generated PHP source code + */ + private function parent($id, $indent, array $children, $level) + { + $realChildren = array_filter($children, array(__CLASS__, 'onlyBlockArgs')); + + if (empty($realChildren)) { + return sprintf($this->prepare(self::PARENT_NO_CONTEXT, $level), var_export($id, true)); + } + + return sprintf( + $this->prepare(self::PARENT, $level), + var_export($id, true), + $this->walk($realChildren, $level + 1) + ); + } + + /** + * Helper method for filtering out non-block-arg tokens. + * + * @param array $node + * + * @return bool True if $node is a block arg token + */ + private static function onlyBlockArgs(array $node) + { + return $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_BLOCK_ARG; + } + + const VARIABLE = ' + $value = $this->resolveValue($context->%s(%s), $context);%s + $buffer .= %s($value === null ? \'\' : %s); + '; + + /** + * Generate Mustache Template variable interpolation PHP source. + * + * @param string $id Variable name + * @param string[] $filters Array of filters + * @param bool $escape Escape the variable value for output? + * @param int $level + * + * @return string Generated variable interpolation PHP source + */ + private function variable($id, $filters, $escape, $level) + { + $method = $this->getFindMethod($id); + $id = ($method !== 'last') ? var_export($id, true) : ''; + $filters = $this->getFilters($filters, $level); + $value = $escape ? $this->getEscape() : '$value'; + + return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value); + } + + const FILTER = ' + $filter = $context->%s(%s); + if (!(%s)) { + throw new Mustache_Exception_UnknownFilterException(%s); + } + $value = call_user_func($filter, $value);%s + '; + + /** + * Generate Mustache Template variable filtering PHP source. + * + * @param string[] $filters Array of filters + * @param int $level + * + * @return string Generated filter PHP source + */ + private function getFilters(array $filters, $level) + { + if (empty($filters)) { + return ''; + } + + $name = array_shift($filters); + $method = $this->getFindMethod($name); + $filter = ($method !== 'last') ? var_export($name, true) : ''; + $callable = $this->getCallable('$filter'); + $msg = var_export($name, true); + + return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $callable, $msg, $this->getFilters($filters, $level)); + } + + const LINE = '$buffer .= "\n";'; + const TEXT = '$buffer .= %s%s;'; + + /** + * Generate Mustache Template output Buffer call PHP source. + * + * @param string $text + * @param int $level + * + * @return string Generated output Buffer call PHP source + */ + private function text($text, $level) + { + $indentNextLine = (substr($text, -1) === "\n"); + $code = sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true)); + $this->indentNextLine = $indentNextLine; + + return $code; + } + + /** + * Prepare PHP source code snippet for output. + * + * @param string $text + * @param int $bonus Additional indent level (default: 0) + * @param bool $prependNewline Prepend a newline to the snippet? (default: true) + * @param bool $appendNewline Append a newline to the snippet? (default: false) + * + * @return string PHP source code snippet + */ + private function prepare($text, $bonus = 0, $prependNewline = true, $appendNewline = false) + { + $text = ($prependNewline ? "\n" : '') . trim($text); + if ($prependNewline) { + $bonus++; + } + if ($appendNewline) { + $text .= "\n"; + } + + return preg_replace("/\n( {8})?/", "\n" . str_repeat(' ', $bonus * 4), $text); + } + + const DEFAULT_ESCAPE = 'htmlspecialchars(%s, %s, %s)'; + const CUSTOM_ESCAPE = 'call_user_func($this->mustache->getEscape(), %s)'; + + /** + * Get the current escaper. + * + * @param string $value (default: '$value') + * + * @return string Either a custom callback, or an inline call to `htmlspecialchars` + */ + private function getEscape($value = '$value') + { + if ($this->customEscape) { + return sprintf(self::CUSTOM_ESCAPE, $value); + } + + return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->entityFlags, true), var_export($this->charset, true)); + } + + /** + * Select the appropriate Context `find` method for a given $id. + * + * The return value will be one of `find`, `findDot`, `findAnchoredDot` or `last`. + * + * @see Mustache_Context::find + * @see Mustache_Context::findDot + * @see Mustache_Context::last + * + * @param string $id Variable name + * + * @return string `find` method name + */ + private function getFindMethod($id) + { + if ($id === '.') { + return 'last'; + } + + if (isset($this->pragmas[Mustache_Engine::PRAGMA_ANCHORED_DOT]) && $this->pragmas[Mustache_Engine::PRAGMA_ANCHORED_DOT]) { + if (substr($id, 0, 1) === '.') { + return 'findAnchoredDot'; + } + } + + if (strpos($id, '.') === false) { + return 'find'; + } + + return 'findDot'; + } + + const IS_CALLABLE = '!is_string(%s) && is_callable(%s)'; + const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)'; + + /** + * Helper function to compile strict vs lax "is callable" logic. + * + * @param string $variable (default: '$value') + * + * @return string "is callable" logic + */ + private function getCallable($variable = '$value') + { + $tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE; + + return sprintf($tpl, $variable, $variable); + } + + const LINE_INDENT = '$indent . '; + + /** + * Get the current $indent prefix to write to the buffer. + * + * @return string "$indent . " or "" + */ + private function flushIndent() + { + if (!$this->indentNextLine) { + return ''; + } + + $this->indentNextLine = false; + + return self::LINE_INDENT; + } +} diff --git a/orcinus/mustache/src/Mustache/Context.php b/orcinus/mustache/src/Mustache/Context.php new file mode 100644 index 0000000..69c02e0 --- /dev/null +++ b/orcinus/mustache/src/Mustache/Context.php @@ -0,0 +1,242 @@ +stack = array($context); + } + } + + /** + * Push a new Context frame onto the stack. + * + * @param mixed $value Object or array to use for context + */ + public function push($value) + { + array_push($this->stack, $value); + } + + /** + * Push a new Context frame onto the block context stack. + * + * @param mixed $value Object or array to use for block context + */ + public function pushBlockContext($value) + { + array_push($this->blockStack, $value); + } + + /** + * Pop the last Context frame from the stack. + * + * @return mixed Last Context frame (object or array) + */ + public function pop() + { + return array_pop($this->stack); + } + + /** + * Pop the last block Context frame from the stack. + * + * @return mixed Last block Context frame (object or array) + */ + public function popBlockContext() + { + return array_pop($this->blockStack); + } + + /** + * Get the last Context frame. + * + * @return mixed Last Context frame (object or array) + */ + public function last() + { + return end($this->stack); + } + + /** + * Find a variable in the Context stack. + * + * Starting with the last Context frame (the context of the innermost section), and working back to the top-level + * rendering context, look for a variable with the given name: + * + * * If the Context frame is an associative array which contains the key $id, returns the value of that element. + * * If the Context frame is an object, this will check first for a public method, then a public property named + * $id. Failing both of these, it will try `__isset` and `__get` magic methods. + * * If a value named $id is not found in any Context frame, returns an empty string. + * + * @param string $id Variable name + * + * @return mixed Variable value, or '' if not found + */ + public function find($id) + { + return $this->findVariableInStack($id, $this->stack); + } + + /** + * Find a 'dot notation' variable in the Context stack. + * + * Note that dot notation traversal bubbles through scope differently than the regular find method. After finding + * the initial chunk of the dotted name, each subsequent chunk is searched for only within the value of the previous + * result. For example, given the following context stack: + * + * $data = array( + * 'name' => 'Fred', + * 'child' => array( + * 'name' => 'Bob' + * ), + * ); + * + * ... and the Mustache following template: + * + * {{ child.name }} + * + * ... the `name` value is only searched for within the `child` value of the global Context, not within parent + * Context frames. + * + * @param string $id Dotted variable selector + * + * @return mixed Variable value, or '' if not found + */ + public function findDot($id) + { + $chunks = explode('.', $id); + $first = array_shift($chunks); + $value = $this->findVariableInStack($first, $this->stack); + + foreach ($chunks as $chunk) { + if ($value === '') { + return $value; + } + + $value = $this->findVariableInStack($chunk, array($value)); + } + + return $value; + } + + /** + * Find an 'anchored dot notation' variable in the Context stack. + * + * This is the same as findDot(), except it looks in the top of the context + * stack for the first value, rather than searching the whole context stack + * and starting from there. + * + * @see Mustache_Context::findDot + * + * @throws Mustache_Exception_InvalidArgumentException if given an invalid anchored dot $id + * + * @param string $id Dotted variable selector + * + * @return mixed Variable value, or '' if not found + */ + public function findAnchoredDot($id) + { + $chunks = explode('.', $id); + $first = array_shift($chunks); + if ($first !== '') { + throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected id for findAnchoredDot: %s', $id)); + } + + $value = $this->last(); + + foreach ($chunks as $chunk) { + if ($value === '') { + return $value; + } + + $value = $this->findVariableInStack($chunk, array($value)); + } + + return $value; + } + + /** + * Find an argument in the block context stack. + * + * @param string $id + * + * @return mixed Variable value, or '' if not found + */ + public function findInBlock($id) + { + foreach ($this->blockStack as $context) { + if (array_key_exists($id, $context)) { + return $context[$id]; + } + } + + return ''; + } + + /** + * Helper function to find a variable in the Context stack. + * + * @see Mustache_Context::find + * + * @param string $id Variable name + * @param array $stack Context stack + * + * @return mixed Variable value, or '' if not found + */ + private function findVariableInStack($id, array $stack) + { + for ($i = count($stack) - 1; $i >= 0; $i--) { + $frame = &$stack[$i]; + + switch (gettype($frame)) { + case 'object': + if (!($frame instanceof Closure)) { + // Note that is_callable() *will not work here* + // See https://github.com/bobthecow/mustache.php/wiki/Magic-Methods + if (method_exists($frame, $id)) { + return $frame->$id(); + } + + if (isset($frame->$id)) { + return $frame->$id; + } + + if ($frame instanceof ArrayAccess && isset($frame[$id])) { + return $frame[$id]; + } + } + break; + + case 'array': + if (array_key_exists($id, $frame)) { + return $frame[$id]; + } + break; + } + } + + return ''; + } +} diff --git a/orcinus/mustache/src/Mustache/Engine.php b/orcinus/mustache/src/Mustache/Engine.php new file mode 100644 index 0000000..bbdc975 --- /dev/null +++ b/orcinus/mustache/src/Mustache/Engine.php @@ -0,0 +1,829 @@ + true, + self::PRAGMA_BLOCKS => true, + self::PRAGMA_ANCHORED_DOT => true, + ); + + // Template cache + private $templates = array(); + + // Environment + private $templateClassPrefix = '__Mustache_'; + private $cache; + private $lambdaCache; + private $cacheLambdaTemplates = false; + private $loader; + private $partialsLoader; + private $helpers; + private $escape; + private $entityFlags = ENT_COMPAT; + private $charset = 'UTF-8'; + private $logger; + private $strictCallables = false; + private $pragmas = array(); + private $delimiters; + + // Services + private $tokenizer; + private $parser; + private $compiler; + + /** + * Mustache class constructor. + * + * Passing an $options array allows overriding certain Mustache options during instantiation: + * + * $options = array( + * // The class prefix for compiled templates. Defaults to '__Mustache_'. + * 'template_class_prefix' => '__MyTemplates_', + * + * // A Mustache cache instance or a cache directory string for compiled templates. + * // Mustache will not cache templates unless this is set. + * 'cache' => dirname(__FILE__).'/tmp/cache/mustache', + * + * // Override default permissions for cache files. Defaults to using the system-defined umask. It is + * // *strongly* recommended that you configure your umask properly rather than overriding permissions here. + * 'cache_file_mode' => 0666, + * + * // Optionally, enable caching for lambda section templates. This is generally not recommended, as lambda + * // sections are often too dynamic to benefit from caching. + * 'cache_lambda_templates' => true, + * + * // Customize the tag delimiters used by this engine instance. Note that overriding here changes the + * // delimiters used to parse all templates and partials loaded by this instance. To override just for a + * // single template, use an inline "change delimiters" tag at the start of the template file: + * // + * // {{=<% %>=}} + * // + * 'delimiters' => '<% %>', + * + * // A Mustache template loader instance. Uses a StringLoader if not specified. + * 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'), + * + * // A Mustache loader instance for partials. + * 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'), + * + * // An array of Mustache partials. Useful for quick-and-dirty string template loading, but not as + * // efficient or lazy as a Filesystem (or database) loader. + * 'partials' => array('foo' => file_get_contents(dirname(__FILE__).'/views/partials/foo.mustache')), + * + * // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order + * // sections), or any other valid Mustache context value. They will be prepended to the context stack, + * // so they will be available in any template loaded by this Mustache instance. + * 'helpers' => array('i18n' => function ($text) { + * // do something translatey here... + * }), + * + * // An 'escape' callback, responsible for escaping double-mustache variables. + * 'escape' => function ($value) { + * return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8'); + * }, + * + * // Type argument for `htmlspecialchars`. Defaults to ENT_COMPAT. You may prefer ENT_QUOTES. + * 'entity_flags' => ENT_QUOTES, + * + * // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'. + * 'charset' => 'ISO-8859-1', + * + * // A Mustache Logger instance. No logging will occur unless this is set. Using a PSR-3 compatible + * // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is + * // available as well: + * 'logger' => new Mustache_Logger_StreamLogger('php://stderr'), + * + * // Only treat Closure instances and invokable classes as callable. If true, values like + * // `array('ClassName', 'methodName')` and `array($classInstance, 'methodName')`, which are traditionally + * // "callable" in PHP, are not called to resolve variables for interpolation or section contexts. This + * // helps protect against arbitrary code execution when user input is passed directly into the template. + * // This currently defaults to false, but will default to true in v3.0. + * 'strict_callables' => true, + * + * // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual + * // templates. + * 'pragmas' => [Mustache_Engine::PRAGMA_FILTERS], + * ); + * + * @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable + * + * @param array $options (default: array()) + */ + public function __construct(array $options = array()) + { + if (isset($options['template_class_prefix'])) { + if ((string) $options['template_class_prefix'] === '') { + throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "template_class_prefix" must not be empty'); + } + + $this->templateClassPrefix = $options['template_class_prefix']; + } + + if (isset($options['cache'])) { + $cache = $options['cache']; + + if (is_string($cache)) { + $mode = isset($options['cache_file_mode']) ? $options['cache_file_mode'] : null; + $cache = new Mustache_Cache_FilesystemCache($cache, $mode); + } + + $this->setCache($cache); + } + + if (isset($options['cache_lambda_templates'])) { + $this->cacheLambdaTemplates = (bool) $options['cache_lambda_templates']; + } + + if (isset($options['loader'])) { + $this->setLoader($options['loader']); + } + + if (isset($options['partials_loader'])) { + $this->setPartialsLoader($options['partials_loader']); + } + + if (isset($options['partials'])) { + $this->setPartials($options['partials']); + } + + if (isset($options['helpers'])) { + $this->setHelpers($options['helpers']); + } + + if (isset($options['escape'])) { + if (!is_callable($options['escape'])) { + throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "escape" option must be callable'); + } + + $this->escape = $options['escape']; + } + + if (isset($options['entity_flags'])) { + $this->entityFlags = $options['entity_flags']; + } + + if (isset($options['charset'])) { + $this->charset = $options['charset']; + } + + if (isset($options['logger'])) { + $this->setLogger($options['logger']); + } + + if (isset($options['strict_callables'])) { + $this->strictCallables = $options['strict_callables']; + } + + if (isset($options['delimiters'])) { + $this->delimiters = $options['delimiters']; + } + + if (isset($options['pragmas'])) { + foreach ($options['pragmas'] as $pragma) { + if (!isset(self::$knownPragmas[$pragma])) { + throw new Mustache_Exception_InvalidArgumentException(sprintf('Unknown pragma: "%s".', $pragma)); + } + $this->pragmas[$pragma] = true; + } + } + } + + /** + * Shortcut 'render' invocation. + * + * Equivalent to calling `$mustache->loadTemplate($template)->render($context);` + * + * @see Mustache_Engine::loadTemplate + * @see Mustache_Template::render + * + * @param string $template + * @param mixed $context (default: array()) + * + * @return string Rendered template + */ + public function render($template, $context = array()) + { + return $this->loadTemplate($template)->render($context); + } + + /** + * Get the current Mustache escape callback. + * + * @return callable|null + */ + public function getEscape() + { + return $this->escape; + } + + /** + * Get the current Mustache entitity type to escape. + * + * @return int + */ + public function getEntityFlags() + { + return $this->entityFlags; + } + + /** + * Get the current Mustache character set. + * + * @return string + */ + public function getCharset() + { + return $this->charset; + } + + /** + * Get the current globally enabled pragmas. + * + * @return array + */ + public function getPragmas() + { + return array_keys($this->pragmas); + } + + /** + * Set the Mustache template Loader instance. + * + * @param Mustache_Loader $loader + */ + public function setLoader(Mustache_Loader $loader) + { + $this->loader = $loader; + } + + /** + * Get the current Mustache template Loader instance. + * + * If no Loader instance has been explicitly specified, this method will instantiate and return + * a StringLoader instance. + * + * @return Mustache_Loader + */ + public function getLoader() + { + if (!isset($this->loader)) { + $this->loader = new Mustache_Loader_StringLoader(); + } + + return $this->loader; + } + + /** + * Set the Mustache partials Loader instance. + * + * @param Mustache_Loader $partialsLoader + */ + public function setPartialsLoader(Mustache_Loader $partialsLoader) + { + $this->partialsLoader = $partialsLoader; + } + + /** + * Get the current Mustache partials Loader instance. + * + * If no Loader instance has been explicitly specified, this method will instantiate and return + * an ArrayLoader instance. + * + * @return Mustache_Loader + */ + public function getPartialsLoader() + { + if (!isset($this->partialsLoader)) { + $this->partialsLoader = new Mustache_Loader_ArrayLoader(); + } + + return $this->partialsLoader; + } + + /** + * Set partials for the current partials Loader instance. + * + * @throws Mustache_Exception_RuntimeException If the current Loader instance is immutable + * + * @param array $partials (default: array()) + */ + public function setPartials(array $partials = array()) + { + if (!isset($this->partialsLoader)) { + $this->partialsLoader = new Mustache_Loader_ArrayLoader(); + } + + if (!$this->partialsLoader instanceof Mustache_Loader_MutableLoader) { + throw new Mustache_Exception_RuntimeException('Unable to set partials on an immutable Mustache Loader instance'); + } + + $this->partialsLoader->setTemplates($partials); + } + + /** + * Set an array of Mustache helpers. + * + * An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or + * any other valid Mustache context value. They will be prepended to the context stack, so they will be available in + * any template loaded by this Mustache instance. + * + * @throws Mustache_Exception_InvalidArgumentException if $helpers is not an array or Traversable + * + * @param array|Traversable $helpers + */ + public function setHelpers($helpers) + { + if (!is_array($helpers) && !$helpers instanceof Traversable) { + throw new Mustache_Exception_InvalidArgumentException('setHelpers expects an array of helpers'); + } + + $this->getHelpers()->clear(); + + foreach ($helpers as $name => $helper) { + $this->addHelper($name, $helper); + } + } + + /** + * Get the current set of Mustache helpers. + * + * @see Mustache_Engine::setHelpers + * + * @return Mustache_HelperCollection + */ + public function getHelpers() + { + if (!isset($this->helpers)) { + $this->helpers = new Mustache_HelperCollection(); + } + + return $this->helpers; + } + + /** + * Add a new Mustache helper. + * + * @see Mustache_Engine::setHelpers + * + * @param string $name + * @param mixed $helper + */ + public function addHelper($name, $helper) + { + $this->getHelpers()->add($name, $helper); + } + + /** + * Get a Mustache helper by name. + * + * @see Mustache_Engine::setHelpers + * + * @param string $name + * + * @return mixed Helper + */ + public function getHelper($name) + { + return $this->getHelpers()->get($name); + } + + /** + * Check whether this Mustache instance has a helper. + * + * @see Mustache_Engine::setHelpers + * + * @param string $name + * + * @return bool True if the helper is present + */ + public function hasHelper($name) + { + return $this->getHelpers()->has($name); + } + + /** + * Remove a helper by name. + * + * @see Mustache_Engine::setHelpers + * + * @param string $name + */ + public function removeHelper($name) + { + $this->getHelpers()->remove($name); + } + + /** + * Set the Mustache Logger instance. + * + * @throws Mustache_Exception_InvalidArgumentException If logger is not an instance of Mustache_Logger or Psr\Log\LoggerInterface + * + * @param Mustache_Logger|Psr\Log\LoggerInterface $logger + */ + public function setLogger($logger = null) + { + if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) { + throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.'); + } + + if ($this->getCache()->getLogger() === null) { + $this->getCache()->setLogger($logger); + } + + $this->logger = $logger; + } + + /** + * Get the current Mustache Logger instance. + * + * @return Mustache_Logger|Psr\Log\LoggerInterface + */ + public function getLogger() + { + return $this->logger; + } + + /** + * Set the Mustache Tokenizer instance. + * + * @param Mustache_Tokenizer $tokenizer + */ + public function setTokenizer(Mustache_Tokenizer $tokenizer) + { + $this->tokenizer = $tokenizer; + } + + /** + * Get the current Mustache Tokenizer instance. + * + * If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one. + * + * @return Mustache_Tokenizer + */ + public function getTokenizer() + { + if (!isset($this->tokenizer)) { + $this->tokenizer = new Mustache_Tokenizer(); + } + + return $this->tokenizer; + } + + /** + * Set the Mustache Parser instance. + * + * @param Mustache_Parser $parser + */ + public function setParser(Mustache_Parser $parser) + { + $this->parser = $parser; + } + + /** + * Get the current Mustache Parser instance. + * + * If no Parser instance has been explicitly specified, this method will instantiate and return a new one. + * + * @return Mustache_Parser + */ + public function getParser() + { + if (!isset($this->parser)) { + $this->parser = new Mustache_Parser(); + } + + return $this->parser; + } + + /** + * Set the Mustache Compiler instance. + * + * @param Mustache_Compiler $compiler + */ + public function setCompiler(Mustache_Compiler $compiler) + { + $this->compiler = $compiler; + } + + /** + * Get the current Mustache Compiler instance. + * + * If no Compiler instance has been explicitly specified, this method will instantiate and return a new one. + * + * @return Mustache_Compiler + */ + public function getCompiler() + { + if (!isset($this->compiler)) { + $this->compiler = new Mustache_Compiler(); + } + + return $this->compiler; + } + + /** + * Set the Mustache Cache instance. + * + * @param Mustache_Cache $cache + */ + public function setCache(Mustache_Cache $cache) + { + if (isset($this->logger) && $cache->getLogger() === null) { + $cache->setLogger($this->getLogger()); + } + + $this->cache = $cache; + } + + /** + * Get the current Mustache Cache instance. + * + * If no Cache instance has been explicitly specified, this method will instantiate and return a new one. + * + * @return Mustache_Cache + */ + public function getCache() + { + if (!isset($this->cache)) { + $this->setCache(new Mustache_Cache_NoopCache()); + } + + return $this->cache; + } + + /** + * Get the current Lambda Cache instance. + * + * If 'cache_lambda_templates' is enabled, this is the default cache instance. Otherwise, it is a NoopCache. + * + * @see Mustache_Engine::getCache + * + * @return Mustache_Cache + */ + protected function getLambdaCache() + { + if ($this->cacheLambdaTemplates) { + return $this->getCache(); + } + + if (!isset($this->lambdaCache)) { + $this->lambdaCache = new Mustache_Cache_NoopCache(); + } + + return $this->lambdaCache; + } + + /** + * Helper method to generate a Mustache template class. + * + * This method must be updated any time options are added which make it so + * the same template could be parsed and compiled multiple different ways. + * + * @param string|Mustache_Source $source + * + * @return string Mustache Template class name + */ + public function getTemplateClassName($source) + { + // For the most part, adding a new option here should do the trick. + // + // Pick a value here which is unique for each possible way the template + // could be compiled... but not necessarily unique per option value. See + // escape below, which only needs to differentiate between 'custom' and + // 'default' escapes. + // + // Keep this list in alphabetical order :) + $chunks = array( + 'charset' => $this->charset, + 'delimiters' => $this->delimiters ? $this->delimiters : '{{ }}', + 'entityFlags' => $this->entityFlags, + 'escape' => isset($this->escape) ? 'custom' : 'default', + 'key' => ($source instanceof Mustache_Source) ? $source->getKey() : 'source', + 'pragmas' => $this->getPragmas(), + 'strictCallables' => $this->strictCallables, + 'version' => self::VERSION, + ); + + $key = json_encode($chunks); + + // Template Source instances have already provided their own source key. For strings, just include the whole + // source string in the md5 hash. + if (!$source instanceof Mustache_Source) { + $key .= "\n" . $source; + } + + return $this->templateClassPrefix . md5($key); + } + + /** + * Load a Mustache Template by name. + * + * @param string $name + * + * @return Mustache_Template + */ + public function loadTemplate($name) + { + return $this->loadSource($this->getLoader()->load($name)); + } + + /** + * Load a Mustache partial Template by name. + * + * This is a helper method used internally by Template instances for loading partial templates. You can most likely + * ignore it completely. + * + * @param string $name + * + * @return Mustache_Template + */ + public function loadPartial($name) + { + try { + if (isset($this->partialsLoader)) { + $loader = $this->partialsLoader; + } elseif (isset($this->loader) && !$this->loader instanceof Mustache_Loader_StringLoader) { + $loader = $this->loader; + } else { + throw new Mustache_Exception_UnknownTemplateException($name); + } + + return $this->loadSource($loader->load($name)); + } catch (Mustache_Exception_UnknownTemplateException $e) { + // If the named partial cannot be found, log then return null. + $this->log( + Mustache_Logger::WARNING, + 'Partial not found: "{name}"', + array('name' => $e->getTemplateName()) + ); + } + } + + /** + * Load a Mustache lambda Template by source. + * + * This is a helper method used by Template instances to generate subtemplates for Lambda sections. You can most + * likely ignore it completely. + * + * @param string $source + * @param string $delims (default: null) + * + * @return Mustache_Template + */ + public function loadLambda($source, $delims = null) + { + if ($delims !== null) { + $source = $delims . "\n" . $source; + } + + return $this->loadSource($source, $this->getLambdaCache()); + } + + /** + * Instantiate and return a Mustache Template instance by source. + * + * Optionally provide a Mustache_Cache instance. This is used internally by Mustache_Engine::loadLambda to respect + * the 'cache_lambda_templates' configuration option. + * + * @see Mustache_Engine::loadTemplate + * @see Mustache_Engine::loadPartial + * @see Mustache_Engine::loadLambda + * + * @param string|Mustache_Source $source + * @param Mustache_Cache $cache (default: null) + * + * @return Mustache_Template + */ + private function loadSource($source, Mustache_Cache $cache = null) + { + $className = $this->getTemplateClassName($source); + + if (!isset($this->templates[$className])) { + if ($cache === null) { + $cache = $this->getCache(); + } + + if (!class_exists($className, false)) { + if (!$cache->load($className)) { + $compiled = $this->compile($source); + $cache->cache($className, $compiled); + } + } + + $this->log( + Mustache_Logger::DEBUG, + 'Instantiating template: "{className}"', + array('className' => $className) + ); + + $this->templates[$className] = new $className($this); + } + + return $this->templates[$className]; + } + + /** + * Helper method to tokenize a Mustache template. + * + * @see Mustache_Tokenizer::scan + * + * @param string $source + * + * @return array Tokens + */ + private function tokenize($source) + { + return $this->getTokenizer()->scan($source, $this->delimiters); + } + + /** + * Helper method to parse a Mustache template. + * + * @see Mustache_Parser::parse + * + * @param string $source + * + * @return array Token tree + */ + private function parse($source) + { + $parser = $this->getParser(); + $parser->setPragmas($this->getPragmas()); + + return $parser->parse($this->tokenize($source)); + } + + /** + * Helper method to compile a Mustache template. + * + * @see Mustache_Compiler::compile + * + * @param string|Mustache_Source $source + * + * @return string generated Mustache template class code + */ + private function compile($source) + { + $name = $this->getTemplateClassName($source); + + $this->log( + Mustache_Logger::INFO, + 'Compiling template to "{className}" class', + array('className' => $name) + ); + + if ($source instanceof Mustache_Source) { + $source = $source->getSource(); + } + $tree = $this->parse($source); + + $compiler = $this->getCompiler(); + $compiler->setPragmas($this->getPragmas()); + + return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags); + } + + /** + * Add a log record if logging is enabled. + * + * @param int $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + private function log($level, $message, array $context = array()) + { + if (isset($this->logger)) { + $this->logger->log($level, $message, $context); + } + } +} diff --git a/orcinus/mustache/src/Mustache/Exception.php b/orcinus/mustache/src/Mustache/Exception.php new file mode 100644 index 0000000..d4001a9 --- /dev/null +++ b/orcinus/mustache/src/Mustache/Exception.php @@ -0,0 +1,18 @@ +token = $token; + if (version_compare(PHP_VERSION, '5.3.0', '>=')) { + parent::__construct($msg, 0, $previous); + } else { + parent::__construct($msg); // @codeCoverageIgnore + } + } + + /** + * @return array + */ + public function getToken() + { + return $this->token; + } +} diff --git a/orcinus/mustache/src/Mustache/Exception/UnknownFilterException.php b/orcinus/mustache/src/Mustache/Exception/UnknownFilterException.php new file mode 100644 index 0000000..0651c17 --- /dev/null +++ b/orcinus/mustache/src/Mustache/Exception/UnknownFilterException.php @@ -0,0 +1,38 @@ +filterName = $filterName; + $message = sprintf('Unknown filter: %s', $filterName); + if (version_compare(PHP_VERSION, '5.3.0', '>=')) { + parent::__construct($message, 0, $previous); + } else { + parent::__construct($message); // @codeCoverageIgnore + } + } + + public function getFilterName() + { + return $this->filterName; + } +} diff --git a/orcinus/mustache/src/Mustache/Exception/UnknownHelperException.php b/orcinus/mustache/src/Mustache/Exception/UnknownHelperException.php new file mode 100644 index 0000000..193be78 --- /dev/null +++ b/orcinus/mustache/src/Mustache/Exception/UnknownHelperException.php @@ -0,0 +1,38 @@ +helperName = $helperName; + $message = sprintf('Unknown helper: %s', $helperName); + if (version_compare(PHP_VERSION, '5.3.0', '>=')) { + parent::__construct($message, 0, $previous); + } else { + parent::__construct($message); // @codeCoverageIgnore + } + } + + public function getHelperName() + { + return $this->helperName; + } +} diff --git a/orcinus/mustache/src/Mustache/Exception/UnknownTemplateException.php b/orcinus/mustache/src/Mustache/Exception/UnknownTemplateException.php new file mode 100644 index 0000000..32a778a --- /dev/null +++ b/orcinus/mustache/src/Mustache/Exception/UnknownTemplateException.php @@ -0,0 +1,38 @@ +templateName = $templateName; + $message = sprintf('Unknown template: %s', $templateName); + if (version_compare(PHP_VERSION, '5.3.0', '>=')) { + parent::__construct($message, 0, $previous); + } else { + parent::__construct($message); // @codeCoverageIgnore + } + } + + public function getTemplateName() + { + return $this->templateName; + } +} diff --git a/orcinus/mustache/src/Mustache/HelperCollection.php b/orcinus/mustache/src/Mustache/HelperCollection.php new file mode 100644 index 0000000..5d8f73c --- /dev/null +++ b/orcinus/mustache/src/Mustache/HelperCollection.php @@ -0,0 +1,172 @@ + $helper` pairs. + * + * @throws Mustache_Exception_InvalidArgumentException if the $helpers argument isn't an array or Traversable + * + * @param array|Traversable $helpers (default: null) + */ + public function __construct($helpers = null) + { + if ($helpers === null) { + return; + } + + if (!is_array($helpers) && !$helpers instanceof Traversable) { + throw new Mustache_Exception_InvalidArgumentException('HelperCollection constructor expects an array of helpers'); + } + + foreach ($helpers as $name => $helper) { + $this->add($name, $helper); + } + } + + /** + * Magic mutator. + * + * @see Mustache_HelperCollection::add + * + * @param string $name + * @param mixed $helper + */ + public function __set($name, $helper) + { + $this->add($name, $helper); + } + + /** + * Add a helper to this collection. + * + * @param string $name + * @param mixed $helper + */ + public function add($name, $helper) + { + $this->helpers[$name] = $helper; + } + + /** + * Magic accessor. + * + * @see Mustache_HelperCollection::get + * + * @param string $name + * + * @return mixed Helper + */ + public function __get($name) + { + return $this->get($name); + } + + /** + * Get a helper by name. + * + * @throws Mustache_Exception_UnknownHelperException If helper does not exist + * + * @param string $name + * + * @return mixed Helper + */ + public function get($name) + { + if (!$this->has($name)) { + throw new Mustache_Exception_UnknownHelperException($name); + } + + return $this->helpers[$name]; + } + + /** + * Magic isset(). + * + * @see Mustache_HelperCollection::has + * + * @param string $name + * + * @return bool True if helper is present + */ + public function __isset($name) + { + return $this->has($name); + } + + /** + * Check whether a given helper is present in the collection. + * + * @param string $name + * + * @return bool True if helper is present + */ + public function has($name) + { + return array_key_exists($name, $this->helpers); + } + + /** + * Magic unset(). + * + * @see Mustache_HelperCollection::remove + * + * @param string $name + */ + public function __unset($name) + { + $this->remove($name); + } + + /** + * Check whether a given helper is present in the collection. + * + * @throws Mustache_Exception_UnknownHelperException if the requested helper is not present + * + * @param string $name + */ + public function remove($name) + { + if (!$this->has($name)) { + throw new Mustache_Exception_UnknownHelperException($name); + } + + unset($this->helpers[$name]); + } + + /** + * Clear the helper collection. + * + * Removes all helpers from this collection + */ + public function clear() + { + $this->helpers = array(); + } + + /** + * Check whether the helper collection is empty. + * + * @return bool True if the collection is empty + */ + public function isEmpty() + { + return empty($this->helpers); + } +} diff --git a/orcinus/mustache/src/Mustache/LambdaHelper.php b/orcinus/mustache/src/Mustache/LambdaHelper.php new file mode 100644 index 0000000..e93dbfa --- /dev/null +++ b/orcinus/mustache/src/Mustache/LambdaHelper.php @@ -0,0 +1,76 @@ + =}}`. (default: null) + */ + public function __construct(Mustache_Engine $mustache, Mustache_Context $context, $delims = null) + { + $this->mustache = $mustache; + $this->context = $context; + $this->delims = $delims; + } + + /** + * Render a string as a Mustache template with the current rendering context. + * + * @param string $string + * + * @return string Rendered template + */ + public function render($string) + { + return $this->mustache + ->loadLambda((string) $string, $this->delims) + ->renderInternal($this->context); + } + + /** + * Render a string as a Mustache template with the current rendering context. + * + * @param string $string + * + * @return string Rendered template + */ + public function __invoke($string) + { + return $this->render($string); + } + + /** + * Get a Lambda Helper with custom delimiters. + * + * @param string $delims Custom delimiters, in the format `{{= <% %> =}}` + * + * @return Mustache_LambdaHelper + */ + public function withDelimiters($delims) + { + return new self($this->mustache, $this->context, $delims); + } +} diff --git a/orcinus/mustache/src/Mustache/Loader.php b/orcinus/mustache/src/Mustache/Loader.php new file mode 100644 index 0000000..23adba1 --- /dev/null +++ b/orcinus/mustache/src/Mustache/Loader.php @@ -0,0 +1,27 @@ + '{{ bar }}', + * 'baz' => 'Hey {{ qux }}!' + * ); + * + * $tpl = $loader->load('foo'); // '{{ bar }}' + * + * The ArrayLoader is used internally as a partials loader by Mustache_Engine instance when an array of partials + * is set. It can also be used as a quick-and-dirty Template loader. + */ +class Mustache_Loader_ArrayLoader implements Mustache_Loader, Mustache_Loader_MutableLoader +{ + private $templates; + + /** + * ArrayLoader constructor. + * + * @param array $templates Associative array of Template source (default: array()) + */ + public function __construct(array $templates = array()) + { + $this->templates = $templates; + } + + /** + * Load a Template. + * + * @throws Mustache_Exception_UnknownTemplateException If a template file is not found + * + * @param string $name + * + * @return string Mustache Template source + */ + public function load($name) + { + if (!isset($this->templates[$name])) { + throw new Mustache_Exception_UnknownTemplateException($name); + } + + return $this->templates[$name]; + } + + /** + * Set an associative array of Template sources for this loader. + * + * @param array $templates + */ + public function setTemplates(array $templates) + { + $this->templates = $templates; + } + + /** + * Set a Template source by name. + * + * @param string $name + * @param string $template Mustache Template source + */ + public function setTemplate($name, $template) + { + $this->templates[$name] = $template; + } +} diff --git a/orcinus/mustache/src/Mustache/Loader/CascadingLoader.php b/orcinus/mustache/src/Mustache/Loader/CascadingLoader.php new file mode 100644 index 0000000..3fb6353 --- /dev/null +++ b/orcinus/mustache/src/Mustache/Loader/CascadingLoader.php @@ -0,0 +1,69 @@ +loaders = array(); + foreach ($loaders as $loader) { + $this->addLoader($loader); + } + } + + /** + * Add a Loader instance. + * + * @param Mustache_Loader $loader + */ + public function addLoader(Mustache_Loader $loader) + { + $this->loaders[] = $loader; + } + + /** + * Load a Template by name. + * + * @throws Mustache_Exception_UnknownTemplateException If a template file is not found + * + * @param string $name + * + * @return string Mustache Template source + */ + public function load($name) + { + foreach ($this->loaders as $loader) { + try { + return $loader->load($name); + } catch (Mustache_Exception_UnknownTemplateException $e) { + // do nothing, check the next loader. + } + } + + throw new Mustache_Exception_UnknownTemplateException($name); + } +} diff --git a/orcinus/mustache/src/Mustache/Loader/FilesystemLoader.php b/orcinus/mustache/src/Mustache/Loader/FilesystemLoader.php new file mode 100644 index 0000000..e366df7 --- /dev/null +++ b/orcinus/mustache/src/Mustache/Loader/FilesystemLoader.php @@ -0,0 +1,135 @@ +load('foo'); // equivalent to `file_get_contents(dirname(__FILE__).'/views/foo.mustache'); + * + * This is probably the most useful Mustache Loader implementation. It can be used for partials and normal Templates: + * + * $m = new Mustache(array( + * 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'), + * 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'), + * )); + */ +class Mustache_Loader_FilesystemLoader implements Mustache_Loader +{ + private $baseDir; + private $extension = '.mustache'; + private $templates = array(); + + /** + * Mustache filesystem Loader constructor. + * + * Passing an $options array allows overriding certain Loader options during instantiation: + * + * $options = array( + * // The filename extension used for Mustache templates. Defaults to '.mustache' + * 'extension' => '.ms', + * ); + * + * @throws Mustache_Exception_RuntimeException if $baseDir does not exist + * + * @param string $baseDir Base directory containing Mustache template files + * @param array $options Array of Loader options (default: array()) + */ + public function __construct($baseDir, array $options = array()) + { + $this->baseDir = $baseDir; + + if (strpos($this->baseDir, '://') === false) { + $this->baseDir = realpath($this->baseDir); + } + + if ($this->shouldCheckPath() && !is_dir($this->baseDir)) { + throw new Mustache_Exception_RuntimeException(sprintf('FilesystemLoader baseDir must be a directory: %s', $baseDir)); + } + + if (array_key_exists('extension', $options)) { + if (empty($options['extension'])) { + $this->extension = ''; + } else { + $this->extension = '.' . ltrim($options['extension'], '.'); + } + } + } + + /** + * Load a Template by name. + * + * $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'); + * $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache"; + * + * @param string $name + * + * @return string Mustache Template source + */ + public function load($name) + { + if (!isset($this->templates[$name])) { + $this->templates[$name] = $this->loadFile($name); + } + + return $this->templates[$name]; + } + + /** + * Helper function for loading a Mustache file by name. + * + * @throws Mustache_Exception_UnknownTemplateException If a template file is not found + * + * @param string $name + * + * @return string Mustache Template source + */ + protected function loadFile($name) + { + $fileName = $this->getFileName($name); + + if ($this->shouldCheckPath() && !file_exists($fileName)) { + throw new Mustache_Exception_UnknownTemplateException($name); + } + + return file_get_contents($fileName); + } + + /** + * Helper function for getting a Mustache template file name. + * + * @param string $name + * + * @return string Template file name + */ + protected function getFileName($name) + { + $fileName = $this->baseDir . '/' . $name; + if (substr($fileName, 0 - strlen($this->extension)) !== $this->extension) { + $fileName .= $this->extension; + } + + return $fileName; + } + + /** + * Only check if baseDir is a directory and requested templates are files if + * baseDir is using the filesystem stream wrapper. + * + * @return bool Whether to check `is_dir` and `file_exists` + */ + protected function shouldCheckPath() + { + return strpos($this->baseDir, '://') === false || strpos($this->baseDir, 'file://') === 0; + } +} diff --git a/orcinus/mustache/src/Mustache/Loader/InlineLoader.php b/orcinus/mustache/src/Mustache/Loader/InlineLoader.php new file mode 100644 index 0000000..ae297fe --- /dev/null +++ b/orcinus/mustache/src/Mustache/Loader/InlineLoader.php @@ -0,0 +1,123 @@ +load('hello'); + * $goodbye = $loader->load('goodbye'); + * + * __halt_compiler(); + * + * @@ hello + * Hello, {{ planet }}! + * + * @@ goodbye + * Goodbye, cruel {{ planet }} + * + * Templates are deliniated by lines containing only `@@ name`. + * + * The InlineLoader is well-suited to micro-frameworks such as Silex: + * + * $app->register(new MustacheServiceProvider, array( + * 'mustache.loader' => new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__) + * )); + * + * $app->get('/{name}', function ($name) use ($app) { + * return $app['mustache']->render('hello', compact('name')); + * }) + * ->value('name', 'world'); + * + * // ... + * + * __halt_compiler(); + * + * @@ hello + * Hello, {{ name }}! + */ +class Mustache_Loader_InlineLoader implements Mustache_Loader +{ + protected $fileName; + protected $offset; + protected $templates; + + /** + * The InlineLoader requires a filename and offset to process templates. + * + * The magic constants `__FILE__` and `__COMPILER_HALT_OFFSET__` are usually + * perfectly suited to the job: + * + * $loader = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__); + * + * Note that this only works if the loader is instantiated inside the same + * file as the inline templates. If the templates are located in another + * file, it would be necessary to manually specify the filename and offset. + * + * @param string $fileName The file to parse for inline templates + * @param int $offset A string offset for the start of the templates. + * This usually coincides with the `__halt_compiler` + * call, and the `__COMPILER_HALT_OFFSET__` + */ + public function __construct($fileName, $offset) + { + if (!is_file($fileName)) { + throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid filename.'); + } + + if (!is_int($offset) || $offset < 0) { + throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid file offset.'); + } + + $this->fileName = $fileName; + $this->offset = $offset; + } + + /** + * Load a Template by name. + * + * @throws Mustache_Exception_UnknownTemplateException If a template file is not found + * + * @param string $name + * + * @return string Mustache Template source + */ + public function load($name) + { + $this->loadTemplates(); + + if (!array_key_exists($name, $this->templates)) { + throw new Mustache_Exception_UnknownTemplateException($name); + } + + return $this->templates[$name]; + } + + /** + * Parse and load templates from the end of a source file. + */ + protected function loadTemplates() + { + if ($this->templates === null) { + $this->templates = array(); + $data = file_get_contents($this->fileName, false, null, $this->offset); + foreach (preg_split("/^@@(?= [\w\d\.]+$)/m", $data, -1) as $chunk) { + if (trim($chunk)) { + list($name, $content) = explode("\n", $chunk, 2); + $this->templates[trim($name)] = trim($content); + } + } + } + } +} diff --git a/orcinus/mustache/src/Mustache/Loader/MutableLoader.php b/orcinus/mustache/src/Mustache/Loader/MutableLoader.php new file mode 100644 index 0000000..57fe5be --- /dev/null +++ b/orcinus/mustache/src/Mustache/Loader/MutableLoader.php @@ -0,0 +1,31 @@ + '.ms', + * 'stat_props' => array('size', 'mtime'), + * ); + * + * Specifying 'stat_props' overrides the stat properties used to invalidate the template cache. By default, this + * uses 'mtime' and 'size', but this can be set to any of the properties supported by stat(): + * + * http://php.net/manual/en/function.stat.php + * + * You can also disable filesystem stat entirely: + * + * $options = array('stat_props' => null); + * + * But with great power comes great responsibility. Namely, if you disable stat-based cache invalidation, + * YOU MUST CLEAR THE TEMPLATE CACHE YOURSELF when your templates change. Make it part of your build or deploy + * process so you don't forget! + * + * @throws Mustache_Exception_RuntimeException if $baseDir does not exist. + * + * @param string $baseDir Base directory containing Mustache template files. + * @param array $options Array of Loader options (default: array()) + */ + public function __construct($baseDir, array $options = array()) + { + parent::__construct($baseDir, $options); + + if (array_key_exists('stat_props', $options)) { + if (empty($options['stat_props'])) { + $this->statProps = array(); + } else { + $this->statProps = $options['stat_props']; + } + } else { + $this->statProps = array('size', 'mtime'); + } + } + + /** + * Helper function for loading a Mustache file by name. + * + * @throws Mustache_Exception_UnknownTemplateException If a template file is not found. + * + * @param string $name + * + * @return Mustache_Source Mustache Template source + */ + protected function loadFile($name) + { + $fileName = $this->getFileName($name); + + if (!file_exists($fileName)) { + throw new Mustache_Exception_UnknownTemplateException($name); + } + + return new Mustache_Source_FilesystemSource($fileName, $this->statProps); + } +} diff --git a/orcinus/mustache/src/Mustache/Loader/StringLoader.php b/orcinus/mustache/src/Mustache/Loader/StringLoader.php new file mode 100644 index 0000000..7012c03 --- /dev/null +++ b/orcinus/mustache/src/Mustache/Loader/StringLoader.php @@ -0,0 +1,39 @@ +load('{{ foo }}'); // '{{ foo }}' + * + * This is the default Template Loader instance used by Mustache: + * + * $m = new Mustache; + * $tpl = $m->loadTemplate('{{ foo }}'); + * echo $tpl->render(array('foo' => 'bar')); // "bar" + */ +class Mustache_Loader_StringLoader implements Mustache_Loader +{ + /** + * Load a Template by source. + * + * @param string $name Mustache Template source + * + * @return string Mustache Template source + */ + public function load($name) + { + return $name; + } +} diff --git a/orcinus/mustache/src/Mustache/Logger.php b/orcinus/mustache/src/Mustache/Logger.php new file mode 100644 index 0000000..cb4037a --- /dev/null +++ b/orcinus/mustache/src/Mustache/Logger.php @@ -0,0 +1,126 @@ +log(Mustache_Logger::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + */ + public function alert($message, array $context = array()) + { + $this->log(Mustache_Logger::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + */ + public function critical($message, array $context = array()) + { + $this->log(Mustache_Logger::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + */ + public function error($message, array $context = array()) + { + $this->log(Mustache_Logger::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + */ + public function warning($message, array $context = array()) + { + $this->log(Mustache_Logger::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + */ + public function notice($message, array $context = array()) + { + $this->log(Mustache_Logger::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + */ + public function info($message, array $context = array()) + { + $this->log(Mustache_Logger::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + */ + public function debug($message, array $context = array()) + { + $this->log(Mustache_Logger::DEBUG, $message, $context); + } +} diff --git a/orcinus/mustache/src/Mustache/Logger/StreamLogger.php b/orcinus/mustache/src/Mustache/Logger/StreamLogger.php new file mode 100644 index 0000000..402a148 --- /dev/null +++ b/orcinus/mustache/src/Mustache/Logger/StreamLogger.php @@ -0,0 +1,194 @@ + 100, + self::INFO => 200, + self::NOTICE => 250, + self::WARNING => 300, + self::ERROR => 400, + self::CRITICAL => 500, + self::ALERT => 550, + self::EMERGENCY => 600, + ); + + protected $level; + protected $stream = null; + protected $url = null; + + /** + * @throws InvalidArgumentException if the logging level is unknown + * + * @param resource|string $stream Resource instance or URL + * @param int $level The minimum logging level at which this handler will be triggered + */ + public function __construct($stream, $level = Mustache_Logger::ERROR) + { + $this->setLevel($level); + + if (is_resource($stream)) { + $this->stream = $stream; + } else { + $this->url = $stream; + } + } + + /** + * Close stream resources. + */ + public function __destruct() + { + if (is_resource($this->stream)) { + fclose($this->stream); + } + } + + /** + * Set the minimum logging level. + * + * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown + * + * @param int $level The minimum logging level which will be written + */ + public function setLevel($level) + { + if (!array_key_exists($level, self::$levels)) { + throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level)); + } + + $this->level = $level; + } + + /** + * Get the current minimum logging level. + * + * @return int + */ + public function getLevel() + { + return $this->level; + } + + /** + * Logs with an arbitrary level. + * + * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown + * + * @param mixed $level + * @param string $message + * @param array $context + */ + public function log($level, $message, array $context = array()) + { + if (!array_key_exists($level, self::$levels)) { + throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level)); + } + + if (self::$levels[$level] >= self::$levels[$this->level]) { + $this->writeLog($level, $message, $context); + } + } + + /** + * Write a record to the log. + * + * @throws Mustache_Exception_LogicException If neither a stream resource nor url is present + * @throws Mustache_Exception_RuntimeException If the stream url cannot be opened + * + * @param int $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + protected function writeLog($level, $message, array $context = array()) + { + if (!is_resource($this->stream)) { + if (!isset($this->url)) { + throw new Mustache_Exception_LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().'); + } + + $this->stream = fopen($this->url, 'a'); + if (!is_resource($this->stream)) { + // @codeCoverageIgnoreStart + throw new Mustache_Exception_RuntimeException(sprintf('The stream or file "%s" could not be opened.', $this->url)); + // @codeCoverageIgnoreEnd + } + } + + fwrite($this->stream, self::formatLine($level, $message, $context)); + } + + /** + * Gets the name of the logging level. + * + * @throws InvalidArgumentException if the logging level is unknown + * + * @param int $level + * + * @return string + */ + protected static function getLevelName($level) + { + return strtoupper($level); + } + + /** + * Format a log line for output. + * + * @param int $level The logging level + * @param string $message The log message + * @param array $context The log context + * + * @return string + */ + protected static function formatLine($level, $message, array $context = array()) + { + return sprintf( + "%s: %s\n", + self::getLevelName($level), + self::interpolateMessage($message, $context) + ); + } + + /** + * Interpolate context values into the message placeholders. + * + * @param string $message + * @param array $context + * + * @return string + */ + protected static function interpolateMessage($message, array $context = array()) + { + if (strpos($message, '{') === false) { + return $message; + } + + // build a replacement array with braces around the context keys + $replace = array(); + foreach ($context as $key => $val) { + $replace['{' . $key . '}'] = $val; + } + + // interpolate replacement values into the the message and return + return strtr($message, $replace); + } +} diff --git a/orcinus/mustache/src/Mustache/Parser.php b/orcinus/mustache/src/Mustache/Parser.php new file mode 100644 index 0000000..0ec4192 --- /dev/null +++ b/orcinus/mustache/src/Mustache/Parser.php @@ -0,0 +1,317 @@ +lineNum = -1; + $this->lineTokens = 0; + $this->pragmas = $this->defaultPragmas; + + $this->pragmaFilters = isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS]); + $this->pragmaBlocks = isset($this->pragmas[Mustache_Engine::PRAGMA_BLOCKS]); + + return $this->buildTree($tokens); + } + + /** + * Enable pragmas across all templates, regardless of the presence of pragma + * tags in the individual templates. + * + * @internal Users should set global pragmas in Mustache_Engine, not here :) + * + * @param string[] $pragmas + */ + public function setPragmas(array $pragmas) + { + $this->pragmas = array(); + foreach ($pragmas as $pragma) { + $this->enablePragma($pragma); + } + $this->defaultPragmas = $this->pragmas; + } + + /** + * Helper method for recursively building a parse tree. + * + * @throws Mustache_Exception_SyntaxException when nesting errors or mismatched section tags are encountered + * + * @param array &$tokens Set of Mustache tokens + * @param array $parent Parent token (default: null) + * + * @return array Mustache Token parse tree + */ + private function buildTree(array &$tokens, array $parent = null) + { + $nodes = array(); + + while (!empty($tokens)) { + $token = array_shift($tokens); + + if ($token[Mustache_Tokenizer::LINE] === $this->lineNum) { + $this->lineTokens++; + } else { + $this->lineNum = $token[Mustache_Tokenizer::LINE]; + $this->lineTokens = 0; + } + + if ($this->pragmaFilters && isset($token[Mustache_Tokenizer::NAME])) { + list($name, $filters) = $this->getNameAndFilters($token[Mustache_Tokenizer::NAME]); + if (!empty($filters)) { + $token[Mustache_Tokenizer::NAME] = $name; + $token[Mustache_Tokenizer::FILTERS] = $filters; + } + } + + switch ($token[Mustache_Tokenizer::TYPE]) { + case Mustache_Tokenizer::T_DELIM_CHANGE: + $this->checkIfTokenIsAllowedInParent($parent, $token); + $this->clearStandaloneLines($nodes, $tokens); + break; + + case Mustache_Tokenizer::T_SECTION: + case Mustache_Tokenizer::T_INVERTED: + $this->checkIfTokenIsAllowedInParent($parent, $token); + $this->clearStandaloneLines($nodes, $tokens); + $nodes[] = $this->buildTree($tokens, $token); + break; + + case Mustache_Tokenizer::T_END_SECTION: + if (!isset($parent)) { + $msg = sprintf( + 'Unexpected closing tag: /%s on line %d', + $token[Mustache_Tokenizer::NAME], + $token[Mustache_Tokenizer::LINE] + ); + throw new Mustache_Exception_SyntaxException($msg, $token); + } + + if ($token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME]) { + $msg = sprintf( + 'Nesting error: %s (on line %d) vs. %s (on line %d)', + $parent[Mustache_Tokenizer::NAME], + $parent[Mustache_Tokenizer::LINE], + $token[Mustache_Tokenizer::NAME], + $token[Mustache_Tokenizer::LINE] + ); + throw new Mustache_Exception_SyntaxException($msg, $token); + } + + $this->clearStandaloneLines($nodes, $tokens); + $parent[Mustache_Tokenizer::END] = $token[Mustache_Tokenizer::INDEX]; + $parent[Mustache_Tokenizer::NODES] = $nodes; + + return $parent; + + case Mustache_Tokenizer::T_PARTIAL: + $this->checkIfTokenIsAllowedInParent($parent, $token); + //store the whitespace prefix for laters! + if ($indent = $this->clearStandaloneLines($nodes, $tokens)) { + $token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE]; + } + $nodes[] = $token; + break; + + case Mustache_Tokenizer::T_PARENT: + $this->checkIfTokenIsAllowedInParent($parent, $token); + $nodes[] = $this->buildTree($tokens, $token); + break; + + case Mustache_Tokenizer::T_BLOCK_VAR: + if ($this->pragmaBlocks) { + // BLOCKS pragma is enabled, let's do this! + if (isset($parent) && $parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) { + $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_BLOCK_ARG; + } + $this->clearStandaloneLines($nodes, $tokens); + $nodes[] = $this->buildTree($tokens, $token); + } else { + // pretend this was just a normal "escaped" token... + $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_ESCAPED; + // TODO: figure out how to figure out if there was a space after this dollar: + $token[Mustache_Tokenizer::NAME] = '$' . $token[Mustache_Tokenizer::NAME]; + $nodes[] = $token; + } + break; + + case Mustache_Tokenizer::T_PRAGMA: + $this->enablePragma($token[Mustache_Tokenizer::NAME]); + // no break + + case Mustache_Tokenizer::T_COMMENT: + $this->clearStandaloneLines($nodes, $tokens); + $nodes[] = $token; + break; + + default: + $nodes[] = $token; + break; + } + } + + if (isset($parent)) { + $msg = sprintf( + 'Missing closing tag: %s opened on line %d', + $parent[Mustache_Tokenizer::NAME], + $parent[Mustache_Tokenizer::LINE] + ); + throw new Mustache_Exception_SyntaxException($msg, $parent); + } + + return $nodes; + } + + /** + * Clear standalone line tokens. + * + * Returns a whitespace token for indenting partials, if applicable. + * + * @param array $nodes Parsed nodes + * @param array $tokens Tokens to be parsed + * + * @return array|null Resulting indent token, if any + */ + private function clearStandaloneLines(array &$nodes, array &$tokens) + { + if ($this->lineTokens > 1) { + // this is the third or later node on this line, so it can't be standalone + return; + } + + $prev = null; + if ($this->lineTokens === 1) { + // this is the second node on this line, so it can't be standalone + // unless the previous node is whitespace. + if ($prev = end($nodes)) { + if (!$this->tokenIsWhitespace($prev)) { + return; + } + } + } + + if ($next = reset($tokens)) { + // If we're on a new line, bail. + if ($next[Mustache_Tokenizer::LINE] !== $this->lineNum) { + return; + } + + // If the next token isn't whitespace, bail. + if (!$this->tokenIsWhitespace($next)) { + return; + } + + if (count($tokens) !== 1) { + // Unless it's the last token in the template, the next token + // must end in newline for this to be standalone. + if (substr($next[Mustache_Tokenizer::VALUE], -1) !== "\n") { + return; + } + } + + // Discard the whitespace suffix + array_shift($tokens); + } + + if ($prev) { + // Return the whitespace prefix, if any + return array_pop($nodes); + } + } + + /** + * Check whether token is a whitespace token. + * + * True if token type is T_TEXT and value is all whitespace characters. + * + * @param array $token + * + * @return bool True if token is a whitespace token + */ + private function tokenIsWhitespace(array $token) + { + if ($token[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_TEXT) { + return preg_match('/^\s*$/', $token[Mustache_Tokenizer::VALUE]); + } + + return false; + } + + /** + * Check whether a token is allowed inside a parent tag. + * + * @throws Mustache_Exception_SyntaxException if an invalid token is found inside a parent tag + * + * @param array|null $parent + * @param array $token + */ + private function checkIfTokenIsAllowedInParent($parent, array $token) + { + if (isset($parent) && $parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) { + throw new Mustache_Exception_SyntaxException('Illegal content in < parent tag', $token); + } + } + + /** + * Split a tag name into name and filters. + * + * @param string $name + * + * @return array [Tag name, Array of filters] + */ + private function getNameAndFilters($name) + { + $filters = array_map('trim', explode('|', $name)); + $name = array_shift($filters); + + return array($name, $filters); + } + + /** + * Enable a pragma. + * + * @param string $name + */ + private function enablePragma($name) + { + $this->pragmas[$name] = true; + + switch ($name) { + case Mustache_Engine::PRAGMA_BLOCKS: + $this->pragmaBlocks = true; + break; + + case Mustache_Engine::PRAGMA_FILTERS: + $this->pragmaFilters = true; + break; + } + } +} diff --git a/orcinus/mustache/src/Mustache/Source.php b/orcinus/mustache/src/Mustache/Source.php new file mode 100644 index 0000000..278c2cb --- /dev/null +++ b/orcinus/mustache/src/Mustache/Source.php @@ -0,0 +1,40 @@ +fileName = $fileName; + $this->statProps = $statProps; + } + + /** + * Get the Source key (used to generate the compiled class name). + * + * @throws Mustache_Exception_RuntimeException when a source file cannot be read + * + * @return string + */ + public function getKey() + { + $chunks = array( + 'fileName' => $this->fileName, + ); + + if (!empty($this->statProps)) { + if (!isset($this->stat)) { + $this->stat = @stat($this->fileName); + } + + if ($this->stat === false) { + throw new Mustache_Exception_RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName)); + } + + foreach ($this->statProps as $prop) { + $chunks[$prop] = $this->stat[$prop]; + } + } + + return json_encode($chunks); + } + + /** + * Get the template Source. + * + * @return string + */ + public function getSource() + { + return file_get_contents($this->fileName); + } +} diff --git a/orcinus/mustache/src/Mustache/Template.php b/orcinus/mustache/src/Mustache/Template.php new file mode 100644 index 0000000..4de8239 --- /dev/null +++ b/orcinus/mustache/src/Mustache/Template.php @@ -0,0 +1,180 @@ +mustache = $mustache; + } + + /** + * Mustache Template instances can be treated as a function and rendered by simply calling them. + * + * $m = new Mustache_Engine; + * $tpl = $m->loadTemplate('Hello, {{ name }}!'); + * echo $tpl(array('name' => 'World')); // "Hello, World!" + * + * @see Mustache_Template::render + * + * @param mixed $context Array or object rendering context (default: array()) + * + * @return string Rendered template + */ + public function __invoke($context = array()) + { + return $this->render($context); + } + + /** + * Render this template given the rendering context. + * + * @param mixed $context Array or object rendering context (default: array()) + * + * @return string Rendered template + */ + public function render($context = array()) + { + return $this->renderInternal( + $this->prepareContextStack($context) + ); + } + + /** + * Internal rendering method implemented by Mustache Template concrete subclasses. + * + * This is where the magic happens :) + * + * NOTE: This method is not part of the Mustache.php public API. + * + * @param Mustache_Context $context + * @param string $indent (default: '') + * + * @return string Rendered template + */ + abstract public function renderInternal(Mustache_Context $context, $indent = ''); + + /** + * Tests whether a value should be iterated over (e.g. in a section context). + * + * In most languages there are two distinct array types: list and hash (or whatever you want to call them). Lists + * should be iterated, hashes should be treated as objects. Mustache follows this paradigm for Ruby, Javascript, + * Java, Python, etc. + * + * PHP, however, treats lists and hashes as one primitive type: array. So Mustache.php needs a way to distinguish + * between between a list of things (numeric, normalized array) and a set of variables to be used as section context + * (associative array). In other words, this will be iterated over: + * + * $items = array( + * array('name' => 'foo'), + * array('name' => 'bar'), + * array('name' => 'baz'), + * ); + * + * ... but this will be used as a section context block: + * + * $items = array( + * 1 => array('name' => 'foo'), + * 'banana' => array('name' => 'bar'), + * 42 => array('name' => 'baz'), + * ); + * + * @param mixed $value + * + * @return bool True if the value is 'iterable' + */ + protected function isIterable($value) + { + switch (gettype($value)) { + case 'object': + return $value instanceof Traversable; + + case 'array': + $i = 0; + foreach ($value as $k => $v) { + if ($k !== $i++) { + return false; + } + } + + return true; + + default: + return false; + } + } + + /** + * Helper method to prepare the Context stack. + * + * Adds the Mustache HelperCollection to the stack's top context frame if helpers are present. + * + * @param mixed $context Optional first context frame (default: null) + * + * @return Mustache_Context + */ + protected function prepareContextStack($context = null) + { + $stack = new Mustache_Context(); + + $helpers = $this->mustache->getHelpers(); + if (!$helpers->isEmpty()) { + $stack->push($helpers); + } + + if (!empty($context)) { + $stack->push($context); + } + + return $stack; + } + + /** + * Resolve a context value. + * + * Invoke the value if it is callable, otherwise return the value. + * + * @param mixed $value + * @param Mustache_Context $context + * + * @return string + */ + protected function resolveValue($value, Mustache_Context $context) + { + if (($this->strictCallables ? is_object($value) : !is_string($value)) && is_callable($value)) { + return $this->mustache + ->loadLambda((string) call_user_func($value)) + ->renderInternal($context); + } + + return $value; + } +} diff --git a/orcinus/mustache/src/Mustache/Tokenizer.php b/orcinus/mustache/src/Mustache/Tokenizer.php new file mode 100644 index 0000000..e59d30b --- /dev/null +++ b/orcinus/mustache/src/Mustache/Tokenizer.php @@ -0,0 +1,378 @@ +'; + const T_PARENT = '<'; + const T_DELIM_CHANGE = '='; + const T_ESCAPED = '_v'; + const T_UNESCAPED = '{'; + const T_UNESCAPED_2 = '&'; + const T_TEXT = '_t'; + const T_PRAGMA = '%'; + const T_BLOCK_VAR = '$'; + const T_BLOCK_ARG = '$arg'; + + // Valid token types + private static $tagTypes = array( + self::T_SECTION => true, + self::T_INVERTED => true, + self::T_END_SECTION => true, + self::T_COMMENT => true, + self::T_PARTIAL => true, + self::T_PARENT => true, + self::T_DELIM_CHANGE => true, + self::T_ESCAPED => true, + self::T_UNESCAPED => true, + self::T_UNESCAPED_2 => true, + self::T_PRAGMA => true, + self::T_BLOCK_VAR => true, + ); + + // Token properties + const TYPE = 'type'; + const NAME = 'name'; + const OTAG = 'otag'; + const CTAG = 'ctag'; + const LINE = 'line'; + const INDEX = 'index'; + const END = 'end'; + const INDENT = 'indent'; + const NODES = 'nodes'; + const VALUE = 'value'; + const FILTERS = 'filters'; + + private $state; + private $tagType; + private $buffer; + private $tokens; + private $seenTag; + private $line; + + private $otag; + private $otagChar; + private $otagLen; + + private $ctag; + private $ctagChar; + private $ctagLen; + + /** + * Scan and tokenize template source. + * + * @throws Mustache_Exception_SyntaxException when mismatched section tags are encountered + * @throws Mustache_Exception_InvalidArgumentException when $delimiters string is invalid + * + * @param string $text Mustache template source to tokenize + * @param string $delimiters Optionally, pass initial opening and closing delimiters (default: empty string) + * + * @return array Set of Mustache tokens + */ + public function scan($text, $delimiters = '') + { + // Setting mbstring.func_overload makes things *really* slow. + // Let's do everyone a favor and scan this string as ASCII instead. + // + // The INI directive was removed in PHP 8.0 so we don't need to check there (and can drop it + // when we remove support for older versions of PHP). + // + // @codeCoverageIgnoreStart + $encoding = null; + if (version_compare(PHP_VERSION, '8.0.0', '<')) { + if (function_exists('mb_internal_encoding') && ini_get('mbstring.func_overload') & 2) { + $encoding = mb_internal_encoding(); + mb_internal_encoding('ASCII'); + } + } + // @codeCoverageIgnoreEnd + + $this->reset(); + + if (is_string($delimiters) && $delimiters = trim($delimiters)) { + $this->setDelimiters($delimiters); + } + + $len = strlen($text); + for ($i = 0; $i < $len; $i++) { + switch ($this->state) { + case self::IN_TEXT: + $char = $text[$i]; + // Test whether it's time to change tags. + if ($char === $this->otagChar && substr($text, $i, $this->otagLen) === $this->otag) { + $i--; + $this->flushBuffer(); + $this->state = self::IN_TAG_TYPE; + } else { + $this->buffer .= $char; + if ($char === "\n") { + $this->flushBuffer(); + $this->line++; + } + } + break; + + case self::IN_TAG_TYPE: + $i += $this->otagLen - 1; + $char = $text[$i + 1]; + if (isset(self::$tagTypes[$char])) { + $tag = $char; + $this->tagType = $tag; + } else { + $tag = null; + $this->tagType = self::T_ESCAPED; + } + + if ($this->tagType === self::T_DELIM_CHANGE) { + $i = $this->changeDelimiters($text, $i); + $this->state = self::IN_TEXT; + } elseif ($this->tagType === self::T_PRAGMA) { + $i = $this->addPragma($text, $i); + $this->state = self::IN_TEXT; + } else { + if ($tag !== null) { + $i++; + } + $this->state = self::IN_TAG; + } + $this->seenTag = $i; + break; + + default: + $char = $text[$i]; + // Test whether it's time to change tags. + if ($char === $this->ctagChar && substr($text, $i, $this->ctagLen) === $this->ctag) { + $token = array( + self::TYPE => $this->tagType, + self::NAME => trim($this->buffer), + self::OTAG => $this->otag, + self::CTAG => $this->ctag, + self::LINE => $this->line, + self::INDEX => ($this->tagType === self::T_END_SECTION) ? $this->seenTag - $this->otagLen : $i + $this->ctagLen, + ); + + if ($this->tagType === self::T_UNESCAPED) { + // Clean up `{{{ tripleStache }}}` style tokens. + if ($this->ctag === '}}') { + if (($i + 2 < $len) && $text[$i + 2] === '}') { + $i++; + } else { + $msg = sprintf( + 'Mismatched tag delimiters: %s on line %d', + $token[self::NAME], + $token[self::LINE] + ); + + throw new Mustache_Exception_SyntaxException($msg, $token); + } + } else { + $lastName = $token[self::NAME]; + if (substr($lastName, -1) === '}') { + $token[self::NAME] = trim(substr($lastName, 0, -1)); + } else { + $msg = sprintf( + 'Mismatched tag delimiters: %s on line %d', + $token[self::NAME], + $token[self::LINE] + ); + + throw new Mustache_Exception_SyntaxException($msg, $token); + } + } + } + + $this->buffer = ''; + $i += $this->ctagLen - 1; + $this->state = self::IN_TEXT; + $this->tokens[] = $token; + } else { + $this->buffer .= $char; + } + break; + } + } + + if ($this->state !== self::IN_TEXT) { + $this->throwUnclosedTagException(); + } + + $this->flushBuffer(); + + // Restore the user's encoding... + // @codeCoverageIgnoreStart + if ($encoding) { + mb_internal_encoding($encoding); + } + // @codeCoverageIgnoreEnd + + return $this->tokens; + } + + /** + * Helper function to reset tokenizer internal state. + */ + private function reset() + { + $this->state = self::IN_TEXT; + $this->tagType = null; + $this->buffer = ''; + $this->tokens = array(); + $this->seenTag = false; + $this->line = 0; + + $this->otag = '{{'; + $this->otagChar = '{'; + $this->otagLen = 2; + + $this->ctag = '}}'; + $this->ctagChar = '}'; + $this->ctagLen = 2; + } + + /** + * Flush the current buffer to a token. + */ + private function flushBuffer() + { + if (strlen($this->buffer) > 0) { + $this->tokens[] = array( + self::TYPE => self::T_TEXT, + self::LINE => $this->line, + self::VALUE => $this->buffer, + ); + $this->buffer = ''; + } + } + + /** + * Change the current Mustache delimiters. Set new `otag` and `ctag` values. + * + * @throws Mustache_Exception_SyntaxException when delimiter string is invalid + * + * @param string $text Mustache template source + * @param int $index Current tokenizer index + * + * @return int New index value + */ + private function changeDelimiters($text, $index) + { + $startIndex = strpos($text, '=', $index) + 1; + $close = '=' . $this->ctag; + $closeIndex = strpos($text, $close, $index); + + if ($closeIndex === false) { + $this->throwUnclosedTagException(); + } + + $token = array( + self::TYPE => self::T_DELIM_CHANGE, + self::LINE => $this->line, + ); + + try { + $this->setDelimiters(trim(substr($text, $startIndex, $closeIndex - $startIndex))); + } catch (Mustache_Exception_InvalidArgumentException $e) { + throw new Mustache_Exception_SyntaxException($e->getMessage(), $token); + } + + $this->tokens[] = $token; + + return $closeIndex + strlen($close) - 1; + } + + /** + * Set the current Mustache `otag` and `ctag` delimiters. + * + * @throws Mustache_Exception_InvalidArgumentException when delimiter string is invalid + * + * @param string $delimiters + */ + private function setDelimiters($delimiters) + { + if (!preg_match('/^\s*(\S+)\s+(\S+)\s*$/', $delimiters, $matches)) { + throw new Mustache_Exception_InvalidArgumentException(sprintf('Invalid delimiters: %s', $delimiters)); + } + + list($_, $otag, $ctag) = $matches; + + $this->otag = $otag; + $this->otagChar = $otag[0]; + $this->otagLen = strlen($otag); + + $this->ctag = $ctag; + $this->ctagChar = $ctag[0]; + $this->ctagLen = strlen($ctag); + } + + /** + * Add pragma token. + * + * Pragmas are hoisted to the front of the template, so all pragma tokens + * will appear at the front of the token list. + * + * @param string $text + * @param int $index + * + * @return int New index value + */ + private function addPragma($text, $index) + { + $end = strpos($text, $this->ctag, $index); + if ($end === false) { + $this->throwUnclosedTagException(); + } + + $pragma = trim(substr($text, $index + 2, $end - $index - 2)); + + // Pragmas are hoisted to the front of the template. + array_unshift($this->tokens, array( + self::TYPE => self::T_PRAGMA, + self::NAME => $pragma, + self::LINE => 0, + )); + + return $end + $this->ctagLen - 1; + } + + private function throwUnclosedTagException() + { + $name = trim($this->buffer); + if ($name !== '') { + $msg = sprintf('Unclosed tag: %s on line %d', $name, $this->line); + } else { + $msg = sprintf('Unclosed tag on line %d', $this->line); + } + + throw new Mustache_Exception_SyntaxException($msg, array( + self::TYPE => $this->tagType, + self::NAME => $name, + self::OTAG => $this->otag, + self::CTAG => $this->ctag, + self::LINE => $this->line, + self::INDEX => $this->seenTag - $this->otagLen, + )); + } +} diff --git a/orcinus/pdfparser/.php-cs-fixer.php b/orcinus/pdfparser/.php-cs-fixer.php new file mode 100644 index 0000000..85f56fe --- /dev/null +++ b/orcinus/pdfparser/.php-cs-fixer.php @@ -0,0 +1,31 @@ +in([ + __DIR__.'/src', + __DIR__.'/tests', + ]) + ->name('*.php') +; + +$config = new Config(); +$config + ->setFinder($finder) + ->setRiskyAllowed(true) + ->setRules([ + '@Symfony' => true, + '@Symfony:risky' => true, + 'array_syntax' => ['syntax' => 'short'], + 'no_empty_phpdoc' => true, + 'no_unused_imports' => true, + 'no_superfluous_phpdoc_tags' => true, + 'ordered_imports' => true, + 'phpdoc_summary' => false, + 'protected_to_private' => false, + ]) +; + +return $config; diff --git a/orcinus/pdfparser/LICENSE.txt b/orcinus/pdfparser/LICENSE.txt new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/orcinus/pdfparser/LICENSE.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/orcinus/pdfparser/Makefile b/orcinus/pdfparser/Makefile new file mode 100644 index 0000000..afeb533 --- /dev/null +++ b/orcinus/pdfparser/Makefile @@ -0,0 +1,18 @@ +install-dev-tools: + composer update --working-dir=dev-tools + +# Workaround to force PHPUnit 7.5.x when running Scrutinizer. +# Scrutinizer fails due to not enough memory when using a newer PHPUnit version (tested with 9.5). +# @see: https://github.com/smalot/pdfparser/issues/410 +# @see: https://github.com/smalot/pdfparser/pull/412 +prepare-for-scrutinizer: + cd dev-tools && sed -e 's/>=7.5/^7.5/g' composer.json > composer.json2 && rm composer.json && mv composer.json2 composer.json + +run-php-cs-fixer: + dev-tools/vendor/bin/php-cs-fixer fix $(ARGS) + +run-phpstan: + dev-tools/vendor/bin/phpstan analyze $(ARGS) + +run-phpunit: + dev-tools/vendor/bin/phpunit $(ARGS) diff --git a/orcinus/pdfparser/README.md b/orcinus/pdfparser/README.md new file mode 100644 index 0000000..ddffef9 --- /dev/null +++ b/orcinus/pdfparser/README.md @@ -0,0 +1,58 @@ +# PDF parser + +[![Version](https://poser.pugx.org/smalot/pdfparser/v)](//packagist.org/packages/smalot/pdfparser) +![CI](https://github.com/smalot/pdfparser/workflows/CI/badge.svg) +![CS](https://github.com/smalot/pdfparser/workflows/CS/badge.svg) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/smalot/pdfparser/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/smalot/pdfparser/?branch=master) +[![Downloads](https://poser.pugx.org/smalot/pdfparser/downloads)](//packagist.org/packages/smalot/pdfparser) + +The `smalot/pdfparser` is a standalone PHP package that provides various tools to extract data from PDF files. + +This library is under **active maintenance**. +There is no active development by the author of this library (at the moment), but we welcome any pull request adding/extending functionality! + +## Features + +- Load/parse objects and headers +- Extract metadata (author, description, ...) +- Extract text from ordered pages +- Support of compressed PDFs +- Support of MAC OS Roman charset encoding +- Handling of hexa and octal encoding in text sections +- Create custom configurations (see [CustomConfig.md](/doc/CustomConfig.md)). + +Currently, secured documents and extracting form data are not supported. + +## License + +This library is under the [LGPLv3 license](https://github.com/smalot/pdfparser/blob/master/LICENSE.txt). + +## Install + +This library requires PHP 7.1+ since [v1](https://github.com/smalot/pdfparser/releases/tag/v1.0.0). +You can install it via [Composer](https://getcomposer.org/): + +```bash +composer require smalot/pdfparser +``` + +In case you can't use Composer, you can include `alt_autoload.php-dist`. It will include all required files automatically. + +## Quick example + +```php +parseFile('/path/to/document.pdf'); + +$text = $pdf->getText(); +echo $text; +``` + +Further usage information can be found [here](/doc/Usage.md). + +## Documentation + +Documentation can be found in the [doc](/doc) folder. diff --git a/orcinus/pdfparser/alt_autoload.php-dist b/orcinus/pdfparser/alt_autoload.php-dist new file mode 100644 index 0000000..366c57c --- /dev/null +++ b/orcinus/pdfparser/alt_autoload.php-dist @@ -0,0 +1,75 @@ + + * @date 2021-02-09 + * + * @license LGPLv3 + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + * + * -------------------------------------------------------------------------------------- + * + * About: + * This file provides an alternative to the Composer-approach. + * Include it into your project and all required files of PDFParser will be loaded automatically. + * Please use it only, if Composer is not available. + * + * How to use: + * 1. include this file as it is OR copy and rename it as you like (and then include it) + * 2. afterwards you can use PDFParser classes + * Done. + */ + +/** + * Loads all files found in a given folder. + * Calls itself recursively for all sub folders. + * + * @param string $dir + */ +function requireFilesOfFolder($dir) +{ + foreach (new DirectoryIterator($dir) as $fileInfo) { + if (!$fileInfo->isDot()) { + if ($fileInfo->isDir()) { + requireFilesOfFolder($fileInfo->getPathname()); + } else { + require_once $fileInfo->getPathname(); + } + } + } +} + +$rootFolder = __DIR__.'/src/Smalot/PdfParser'; + +// Manually require files, which can't be loaded automatically that easily. +require_once $rootFolder.'/Element.php'; +require_once $rootFolder.'/PDFObject.php'; +require_once $rootFolder.'/Font.php'; +require_once $rootFolder.'/Page.php'; +require_once $rootFolder.'/Element/ElementString.php'; +require_once $rootFolder.'/Encoding/AbstractEncoding.php'; + +/* + * Load the rest of PDFParser files from /src/Smalot/PDFParser + * Dont worry, it wont load files multiple times. + */ +requireFilesOfFolder($rootFolder); diff --git a/orcinus/pdfparser/composer.json b/orcinus/pdfparser/composer.json new file mode 100644 index 0000000..e9f425f --- /dev/null +++ b/orcinus/pdfparser/composer.json @@ -0,0 +1,37 @@ +{ + "name": "smalot/pdfparser", + "description": "Pdf parser library. Can read and extract information from pdf file.", + "keywords": ["PDF", "text", "parser", "parse", "extract"], + "type": "library", + "license": "LGPL-3.0", + "authors": [ + { + "name": "Sebastien MALOT", + "email": "sebastien@malot.fr" + } + ], + "support": { + "issues": "https://github.com/smalot/pdfparser/issues" + }, + "homepage": "https://www.pdfparser.org", + "require": { + "php": ">=7.1", + "symfony/polyfill-mbstring": "^1.18", + "ext-zlib": "*", + "ext-iconv": "*" + }, + "autoload": { + "psr-0": { + "Smalot\\PdfParser\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "PerformanceTests\\": "tests/Performance/", + "PHPUnitTests\\": "tests/PHPUnit/" + } + }, + "config": { + "process-timeout": 1200 + } +} diff --git a/orcinus/pdfparser/doc/CustomConfig.md b/orcinus/pdfparser/doc/CustomConfig.md new file mode 100644 index 0000000..34d5c1c --- /dev/null +++ b/orcinus/pdfparser/doc/CustomConfig.md @@ -0,0 +1,65 @@ +# Configuring the behavior of the parser + +To change the behavior of the parser, create a `Config` object and pass it to the parser. +In this case, we're setting the font space limit. +Changing this value can be helpful when `getText()` returns a text with too many spaces. + +```php +$config = new \Smalot\PdfParser\Config(); +$config->setFontSpaceLimit(-60); +$parser = new \Smalot\PdfParser\Parser([], $config); +$pdf = $parser->parseFile('document.pdf'); +// output extracted text +// echo $pdf->getText(); +``` + +## Config options overview + +The `Config` class has the following options: + +| Option | Type | Default | Description | +|--------------------------|---------|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------| +| `setDecodeMemoryLimit` | Integer | `0` | If parsing fails because of memory exhaustion, you can set a lower memory limit for decoding operations. | +| `setFontSpaceLimit` | Integer | `-50` | Changing font space limit can be helpful when `Parser::getText()` returns a text with too many spaces. | +| `setHorizontalOffset` | String | ` ` | When words are broken up or when the structure of a table is not preserved, you may get better results when adapting `setHorizontalOffset`. | +| `setPdfWhitespaces` | String | `\0\t\n\f\r ` | | +| `setPdfWhitespacesRegex` | String | `[\0\t\n\f\r ]` | | +| `setRetainImageContent` | Boolean | `true` | If parsing fails because of memory exhaustion, you can set the value to `false`. It wont retain image content anymore, but will use less memory too. | + + +## option setDecodeMemoryLimit + setRetainImageContent (manage memory usage) + +If parsing fails because of memory exhaustion, you can use the following options. + +```php +$config = new \Smalot\PdfParser\Config(); +// Whether to retain raw image data as content or discard it to save memory +$config->setRetainImageContent(false); +// Memory limit to use when de-compressing files, in bytes +$config->setDecodeMemoryLimit(1000000); +$parser = new \Smalot\PdfParser\Parser([], $config); +``` + +## option setHorizontalOffset + +When words are broken up or when the structure of a table is not preserved, you can use `setHorizontalOffset`. + +```php +$config = new \Smalot\PdfParser\Config(); +// An empty string can prevent words from breaking up +$config->setHorizontalOffset(''); +// A tab can help preserve the structure of your document +$config->setHorizontalOffset("\t"); +$parser = new \Smalot\PdfParser\Parser([], $config); +``` + +## option setFontSpaceLimit + +Changing font space limit can be helpful when `getText()` returns a text with too many spaces. + +```php +$config = new \Smalot\PdfParser\Config(); +$config->setFontSpaceLimit(-60); +$parser = new \Smalot\PdfParser\Parser([], $config); +$pdf = $parser->parseFile('document.pdf'); +``` diff --git a/orcinus/pdfparser/doc/Developer.md b/orcinus/pdfparser/doc/Developer.md new file mode 100644 index 0000000..e108a82 --- /dev/null +++ b/orcinus/pdfparser/doc/Developer.md @@ -0,0 +1,57 @@ +# Developers + +Here you will find information about our development tools and how to use them. + +## .editorconfig + +Please make sure your editor uses our `.editorconfig` file. It contains rules about our coding styles. + +## GitHub Action Workflows + +We use GitHub Actions to run our continuous integration as well as other tasks after pushing changes. +You will find related files in `.github/workflows/`. + +## Development Tools and Tests + +Our test related files are located in `tests` folder. +Tests are written using PHPUnit. + +To install (and update) development tools like PHPUnit or PHP-CS-Fixer run: + +```bash +make install-dev-tools +``` + +Development tools are getting installed in `dev-tools/vendor`. +Please check `dev-tools/composer.json` for more information about versions etc. +To run a tool manually, you use `dev-tools/vendor/bin`, for instance: + +```bash +dev-tools/vendor/bin/php-cs-fixer fix --verbose --dry-run +``` + +Below are a few shortcuts to improve your developer experience. + +### PHPUnit + +To run all tests run: + +```bash +make run-phpunit +``` + +### PHP-CS-Fixer + +To check coding styles, run: + +```bash +make run-php-cs-fixer +``` + +### PHPStan + +To run a static code analysis, use: + +```bash +make run-phpstan +``` diff --git a/orcinus/pdfparser/doc/Usage.md b/orcinus/pdfparser/doc/Usage.md new file mode 100644 index 0000000..261b975 --- /dev/null +++ b/orcinus/pdfparser/doc/Usage.md @@ -0,0 +1,173 @@ +# Usage + +First create a parser object and point it to a file. + +```php +$parser = new \Smalot\PdfParser\Parser(); + +$pdf = $parser->parseFile('document.pdf'); +// .. or ... +$pdf = $parser->parseContent(file_get_contents('document.pdf')) + ``` + +## Extract text + +A common scenario is to extract text. + +```php +// extract text of the whole PDF +$text = $pdf->getText(); + +// or extract the text of a specific page (in this case the first page) +$text = $pdf->getPages()[0]->getText(); + +// you can also extract text of a limited amount of pages. here, it will only use the first five pages. +$text = $pdf->getText(5); +``` + +## Extract text positions + +You can extract transformation matrix (indexes 0-3) and x,y position of text objects (indexes 4,5). + +```php +$data = $pdf->getPages()[0]->getDataTm(); + +Array +( + [0] => Array + ( + [0] => Array + ( + [0] => 0.999429 + [1] => 0 + [2] => 0 + [3] => 1 + [4] => 201.96 + [5] => 720.68 + ) + + [1] => Document title + ) + + [1] => Array + ( + [0] => Array + ( + [0] => 0.999402 + [1] => 0 + [2] => 0 + [3] => 1 + [4] => 70.8 + [5] => 673.64 + ) + + [1] => Calibri : Lorem ipsum dolor sit amet, consectetur a + ) +) +``` + +When activated via Config setting (`Config::setDataTmFontInfoHasToBeIncluded(true)`) font identifier (index 2) and font size (index 3) are added to dataTm. + +```php +// create config +$config = new Smalot\PdfParser\Config(); +$config->setDataTmFontInfoHasToBeIncluded(true); + +// use config and parse file +$parser = new Smalot\PdfParser\Parser([], $config); +$pdf = $parser->parseFile('document.pdf'); + +$data = $pdf->getPages()[0]->getDataTm(); + +Array +( + [0] => Array + ( + [0] => Array + ( + [0] => 0.999429 + [1] => 0 + [2] => 0 + [3] => 1 + [4] => 201.96 + [5] => 720.68 + ) + + [1] => Document title + [2] => R7 + [3] => 27.96 + ) + + [1] => Array + ( + [0] => Array + ( + [0] => 0.999402 + [1] => 0 + [2] => 0 + [3] => 1 + [4] => 70.8 + [5] => 673.64 + ) + + [1] => Calibri : Lorem ipsum dolor sit amet, consectetur a + [2] => R9 + [3] => 11.04 + ) +) +``` + +Text width should be calculated on text from dataTm to make sure all character widths are available. +In next example we are using data from above. + +```php +$fonts = $pdf->getFonts(); +$font_id = $data[0][2]; //R7 +$font = $fonts[$font_id]; +$text = $data[0][1]; +$width = $font->calculateTextWidth($text, $missing); +``` + +## Extract metadata + +You can also extract metadata. The available data varies from PDF to PDF. + +```php +$metaData = $pdf->getDetails(); + +Array +( + [Producer] => Adobe Acrobat + [CreatedOn] => 2022-01-28T16:36:11+00:00 + [Pages] => 35 +) +``` + +## Read Base64 encoded PDFs + +If working with [Base64](https://en.wikipedia.org/wiki/Base64) encoded PDFs, you might want to parse the PDF without saving the file to disk. +This sample will parse the Base64 encoded PDF and extract text from each page. + +```php +parseContent(base64_decode($base64PDF)); + +$text = $pdf->getText(); +echo $text; +``` + +## Calculate text width + +Try to calculate text width for given font. +Characters without width are added to `$missing` array in second parameter. + +```php +$parser = new \Smalot\PdfParser\Parser(); +$pdf = $parser->parseFile('document.pdf'); +// get first font (we assume here there is at least one) +$font = reset($pdf->getFonts()); +// get width +$width = $font->calculateTextWidth('Some text', $missing); +``` diff --git a/orcinus/pdfparser/phpunit-windows.xml b/orcinus/pdfparser/phpunit-windows.xml new file mode 100644 index 0000000..0efef1d --- /dev/null +++ b/orcinus/pdfparser/phpunit-windows.xml @@ -0,0 +1,21 @@ + + + + + + src + + + + + + + + + + + + tests/PHPUnit + + + diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Config.php b/orcinus/pdfparser/src/Smalot/PdfParser/Config.php new file mode 100644 index 0000000..ff69d3e --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Config.php @@ -0,0 +1,154 @@ + + * + * @date 2020-11-22 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser; + +/** + * This class contains configurations used in various classes. You can override them + * manually, in case default values aren't working. + * + * @see https://github.com/smalot/pdfparser/issues/305 + */ +class Config +{ + private $fontSpaceLimit = -50; + + /** + * @var string + */ + private $horizontalOffset = ' '; + + /** + * Represents: (NUL, HT, LF, FF, CR, SP) + * + * @var string + */ + private $pdfWhitespaces = "\0\t\n\f\r "; + + /** + * Represents: (NUL, HT, LF, FF, CR, SP) + * + * @var string + */ + private $pdfWhitespacesRegex = '[\0\t\n\f\r ]'; + + /** + * Whether to retain raw image data as content or discard it to save memory + * + * @var bool + */ + private $retainImageContent = true; + + /** + * Memory limit to use when de-compressing files, in bytes. + * + * @var int + */ + private $decodeMemoryLimit = 0; + + /** + * Whether to include font id and size in dataTm array + * + * @var bool + */ + private $dataTmFontInfoHasToBeIncluded = false; + + public function getFontSpaceLimit() + { + return $this->fontSpaceLimit; + } + + public function setFontSpaceLimit($value) + { + $this->fontSpaceLimit = $value; + } + + public function getHorizontalOffset(): string + { + return $this->horizontalOffset; + } + + public function setHorizontalOffset($value): void + { + $this->horizontalOffset = $value; + } + + public function getPdfWhitespaces(): string + { + return $this->pdfWhitespaces; + } + + public function setPdfWhitespaces(string $pdfWhitespaces): void + { + $this->pdfWhitespaces = $pdfWhitespaces; + } + + public function getPdfWhitespacesRegex(): string + { + return $this->pdfWhitespacesRegex; + } + + public function setPdfWhitespacesRegex(string $pdfWhitespacesRegex): void + { + $this->pdfWhitespacesRegex = $pdfWhitespacesRegex; + } + + public function getRetainImageContent(): bool + { + return $this->retainImageContent; + } + + public function setRetainImageContent(bool $retainImageContent): void + { + $this->retainImageContent = $retainImageContent; + } + + public function getDecodeMemoryLimit(): int + { + return $this->decodeMemoryLimit; + } + + public function setDecodeMemoryLimit(int $decodeMemoryLimit): void + { + $this->decodeMemoryLimit = $decodeMemoryLimit; + } + + public function getDataTmFontInfoHasToBeIncluded(): bool + { + return $this->dataTmFontInfoHasToBeIncluded; + } + + public function setDataTmFontInfoHasToBeIncluded(bool $dataTmFontInfoHasToBeIncluded): void + { + $this->dataTmFontInfoHasToBeIncluded = $dataTmFontInfoHasToBeIncluded; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Document.php b/orcinus/pdfparser/src/Smalot/PdfParser/Document.php new file mode 100644 index 0000000..9c6835c --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Document.php @@ -0,0 +1,306 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser; + +/** + * Technical references : + * - http://www.mactech.com/articles/mactech/Vol.15/15.09/PDFIntro/index.html + * - http://framework.zend.com/issues/secure/attachment/12512/Pdf.php + * - http://www.php.net/manual/en/ref.pdf.php#74211 + * - http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/ISOLatin1Encoding.pm + * - http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/ISOLatin9Encoding.pm + * - http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/StandardEncoding.pm + * - http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/WinAnsiEncoding.pm + * + * Class Document + */ +class Document +{ + /** + * @var PDFObject[] + */ + protected $objects = []; + + /** + * @var array + */ + protected $dictionary = []; + + /** + * @var Header + */ + protected $trailer = null; + + /** + * @var array + */ + protected $details = null; + + public function __construct() + { + $this->trailer = new Header([], $this); + } + + public function init() + { + $this->buildDictionary(); + + $this->buildDetails(); + + // Propagate init to objects. + foreach ($this->objects as $object) { + $object->getHeader()->init(); + $object->init(); + } + } + + /** + * Build dictionary based on type header field. + */ + protected function buildDictionary() + { + // Build dictionary. + $this->dictionary = []; + + foreach ($this->objects as $id => $object) { + // Cache objects by type and subtype + $type = $object->getHeader()->get('Type')->getContent(); + + if (null != $type) { + if (!isset($this->dictionary[$type])) { + $this->dictionary[$type] = [ + 'all' => [], + 'subtype' => [], + ]; + } + + $this->dictionary[$type]['all'][$id] = $object; + + $subtype = $object->getHeader()->get('Subtype')->getContent(); + if (null != $subtype) { + if (!isset($this->dictionary[$type]['subtype'][$subtype])) { + $this->dictionary[$type]['subtype'][$subtype] = []; + } + $this->dictionary[$type]['subtype'][$subtype][$id] = $object; + } + } + } + } + + /** + * Build details array. + */ + protected function buildDetails() + { + // Build details array. + $details = []; + + // Extract document info + if ($this->trailer->has('Info')) { + /** @var PDFObject $info */ + $info = $this->trailer->get('Info'); + // This could be an ElementMissing object, so we need to check for + // the getHeader method first. + if (null !== $info && method_exists($info, 'getHeader')) { + $details = $info->getHeader()->getDetails(); + } + } + + // Retrieve the page count + try { + $pages = $this->getPages(); + $details['Pages'] = \count($pages); + } catch (\Exception $e) { + $details['Pages'] = 0; + } + + $this->details = $details; + } + + public function getDictionary(): array + { + return $this->dictionary; + } + + /** + * @param PDFObject[] $objects + */ + public function setObjects($objects = []) + { + $this->objects = (array) $objects; + + $this->init(); + } + + /** + * @return PDFObject[] + */ + public function getObjects() + { + return $this->objects; + } + + /** + * @return PDFObject|Font|Page|Element|null + */ + public function getObjectById(string $id) + { + if (isset($this->objects[$id])) { + return $this->objects[$id]; + } + + return null; + } + + public function hasObjectsByType(string $type, ?string $subtype = null): bool + { + return 0 < \count($this->getObjectsByType($type, $subtype)); + } + + public function getObjectsByType(string $type, ?string $subtype = null): array + { + if (!isset($this->dictionary[$type])) { + return []; + } + + if (null != $subtype) { + if (!isset($this->dictionary[$type]['subtype'][$subtype])) { + return []; + } + + return $this->dictionary[$type]['subtype'][$subtype]; + } + + return $this->dictionary[$type]['all']; + } + + /** + * @return Font[] + */ + public function getFonts() + { + return $this->getObjectsByType('Font'); + } + + public function getFirstFont(): ?Font + { + $fonts = $this->getFonts(); + if ([] === $fonts) { + return null; + } + + return reset($fonts); + } + + /** + * @return Page[] + * + * @throws \Exception + */ + public function getPages() + { + if ($this->hasObjectsByType('Catalog')) { + // Search for catalog to list pages. + $catalogues = $this->getObjectsByType('Catalog'); + $catalogue = reset($catalogues); + + /** @var Pages $object */ + $object = $catalogue->get('Pages'); + if (method_exists($object, 'getPages')) { + return $object->getPages(true); + } + } + + if ($this->hasObjectsByType('Pages')) { + // Search for pages to list kids. + $pages = []; + + /** @var Pages[] $objects */ + $objects = $this->getObjectsByType('Pages'); + foreach ($objects as $object) { + $pages = array_merge($pages, $object->getPages(true)); + } + + return $pages; + } + + if ($this->hasObjectsByType('Page')) { + // Search for 'page' (unordered pages). + $pages = $this->getObjectsByType('Page'); + + return array_values($pages); + } + + throw new \Exception('Missing catalog.'); + } + + public function getText(?int $pageLimit = null): string + { + $texts = []; + $pages = $this->getPages(); + + // Only use the first X number of pages if $pageLimit is set and numeric. + if (\is_int($pageLimit) && 0 < $pageLimit) { + $pages = \array_slice($pages, 0, $pageLimit); + } + + foreach ($pages as $index => $page) { + /** + * In some cases, the $page variable may be null. + */ + if (null === $page) { + continue; + } + if ($text = trim($page->getText())) { + $texts[] = $text; + } + } + + return implode("\n\n", $texts); + } + + public function getTrailer(): Header + { + return $this->trailer; + } + + public function setTrailer(Header $trailer) + { + $this->trailer = $trailer; + } + + public function getDetails(): array + { + return $this->details; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Element.php b/orcinus/pdfparser/src/Smalot/PdfParser/Element.php new file mode 100644 index 0000000..4acfcf8 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Element.php @@ -0,0 +1,150 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser; + +use Smalot\PdfParser\Element\ElementArray; +use Smalot\PdfParser\Element\ElementBoolean; +use Smalot\PdfParser\Element\ElementDate; +use Smalot\PdfParser\Element\ElementHexa; +use Smalot\PdfParser\Element\ElementName; +use Smalot\PdfParser\Element\ElementNull; +use Smalot\PdfParser\Element\ElementNumeric; +use Smalot\PdfParser\Element\ElementString; +use Smalot\PdfParser\Element\ElementStruct; +use Smalot\PdfParser\Element\ElementXRef; + +/** + * Class Element + */ +class Element +{ + /** + * @var Document + */ + protected $document = null; + + protected $value = null; + + public function __construct($value, ?Document $document = null) + { + $this->value = $value; + $this->document = $document; + } + + public function init() + { + } + + public function equals($value): bool + { + return $value == $this->value; + } + + public function contains($value): bool + { + if (\is_array($this->value)) { + /** @var Element $val */ + foreach ($this->value as $val) { + if ($val->equals($value)) { + return true; + } + } + + return false; + } + + return $this->equals($value); + } + + public function getContent() + { + return $this->value; + } + + public function __toString(): string + { + return (string) $this->value; + } + + public static function parse(string $content, ?Document $document = null, int &$position = 0) + { + $args = \func_get_args(); + $only_values = isset($args[3]) ? $args[3] : false; + $content = trim($content); + $values = []; + + do { + $old_position = $position; + + if (!$only_values) { + if (!preg_match('/^\s*(?P\/[A-Z0-9\._]+)(?P.*)/si', substr($content, $position), $match)) { + break; + } else { + $name = ltrim($match['name'], '/'); + $value = $match['value']; + $position = strpos($content, $value, $position + \strlen($match['name'])); + } + } else { + $name = \count($values); + $value = substr($content, $position); + } + + if ($element = ElementName::parse($value, $document, $position)) { + $values[$name] = $element; + } elseif ($element = ElementXRef::parse($value, $document, $position)) { + $values[$name] = $element; + } elseif ($element = ElementNumeric::parse($value, $document, $position)) { + $values[$name] = $element; + } elseif ($element = ElementStruct::parse($value, $document, $position)) { + $values[$name] = $element; + } elseif ($element = ElementBoolean::parse($value, $document, $position)) { + $values[$name] = $element; + } elseif ($element = ElementNull::parse($value, $document, $position)) { + $values[$name] = $element; + } elseif ($element = ElementDate::parse($value, $document, $position)) { + $values[$name] = $element; + } elseif ($element = ElementString::parse($value, $document, $position)) { + $values[$name] = $element; + } elseif ($element = ElementHexa::parse($value, $document, $position)) { + $values[$name] = $element; + } elseif ($element = ElementArray::parse($value, $document, $position)) { + $values[$name] = $element; + } else { + $position = $old_position; + break; + } + } while ($position < \strlen($content)); + + return $values; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementArray.php b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementArray.php new file mode 100644 index 0000000..b54bf84 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementArray.php @@ -0,0 +1,139 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Element; + +use Smalot\PdfParser\Document; +use Smalot\PdfParser\Element; +use Smalot\PdfParser\Header; +use Smalot\PdfParser\PDFObject; + +/** + * Class ElementArray + */ +class ElementArray extends Element +{ + public function __construct($value, ?Document $document = null) + { + parent::__construct($value, $document); + } + + public function getContent() + { + foreach ($this->value as $name => $element) { + $this->resolveXRef($name); + } + + return parent::getContent(); + } + + public function getRawContent(): array + { + return $this->value; + } + + public function getDetails(bool $deep = true): array + { + $values = []; + $elements = $this->getContent(); + + foreach ($elements as $key => $element) { + if ($element instanceof Header && $deep) { + $values[$key] = $element->getDetails($deep); + } elseif ($element instanceof PDFObject && $deep) { + $values[$key] = $element->getDetails(false); + } elseif ($element instanceof self) { + if ($deep) { + $values[$key] = $element->getDetails(); + } + } elseif ($element instanceof Element && !($element instanceof self)) { + $values[$key] = $element->getContent(); + } + } + + return $values; + } + + public function __toString(): string + { + return implode(',', $this->value); + } + + /** + * @return Element|PDFObject + */ + protected function resolveXRef(string $name) + { + if (($obj = $this->value[$name]) instanceof ElementXRef) { + /** @var ElementXRef $obj */ + $obj = $this->document->getObjectById($obj->getId()); + $this->value[$name] = $obj; + } + + return $this->value[$name]; + } + + /** + * @todo: These methods return mixed and mismatched types throughout the hierarchy + * + * @return bool|ElementArray + */ + public static function parse(string $content, ?Document $document = null, int &$offset = 0) + { + if (preg_match('/^\s*\[(?P.*)/is', $content, $match)) { + preg_match_all('/(.*?)(\[|\])/s', trim($content), $matches); + + $level = 0; + $sub = ''; + foreach ($matches[0] as $part) { + $sub .= $part; + $level += (false !== strpos($part, '[') ? 1 : -1); + if ($level <= 0) { + break; + } + } + + // Removes 1 level [ and ]. + $sub = substr(trim($sub), 1, -1); + $sub_offset = 0; + $values = Element::parse($sub, $document, $sub_offset, true); + + $offset += strpos($content, '[') + 1; + // Find next ']' position + $offset += \strlen($sub) + 1; + + return new self($values, $document); + } + + return false; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementBoolean.php b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementBoolean.php new file mode 100644 index 0000000..55fb463 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementBoolean.php @@ -0,0 +1,75 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Element; + +use Smalot\PdfParser\Document; +use Smalot\PdfParser\Element; + +/** + * Class ElementBoolean + */ +class ElementBoolean extends Element +{ + /** + * @param string|bool $value + */ + public function __construct($value) + { + parent::__construct('true' == strtolower($value) || true === $value, null); + } + + public function __toString(): string + { + return $this->value ? 'true' : 'false'; + } + + public function equals($value): bool + { + return $this->getContent() === $value; + } + + /** + * @return bool|ElementBoolean + */ + public static function parse(string $content, ?Document $document = null, int &$offset = 0) + { + if (preg_match('/^\s*(?Ptrue|false)/is', $content, $match)) { + $value = $match['value']; + $offset += strpos($content, $value) + \strlen($value); + + return new self($value); + } + + return false; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementDate.php b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementDate.php new file mode 100644 index 0000000..5f3d29f --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementDate.php @@ -0,0 +1,139 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHPi, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Element; + +use Smalot\PdfParser\Document; + +/** + * Class ElementDate + */ +class ElementDate extends ElementString +{ + /** + * @var array + */ + protected static $formats = [ + 4 => 'Y', + 6 => 'Ym', + 8 => 'Ymd', + 10 => 'YmdH', + 12 => 'YmdHi', + 14 => 'YmdHis', + 15 => 'YmdHise', + 17 => 'YmdHisO', + 18 => 'YmdHisO', + 19 => 'YmdHisO', + ]; + + /** + * @var string + */ + protected $format = 'c'; + + /** + * @var \DateTime + */ + protected $value; + + public function __construct($value) + { + if (!($value instanceof \DateTime)) { + throw new \Exception('DateTime required.'); // FIXME: Sometimes strings are passed to this function + } + + parent::__construct($value); + } + + public function setFormat(string $format) + { + $this->format = $format; + } + + public function equals($value): bool + { + if ($value instanceof \DateTime) { + $timestamp = $value->getTimeStamp(); + } else { + $timestamp = strtotime($value); + } + + return $timestamp == $this->value->getTimeStamp(); + } + + public function __toString(): string + { + return (string) $this->value->format($this->format); + } + + /** + * @return bool|ElementDate + */ + public static function parse(string $content, ?Document $document = null, int &$offset = 0) + { + if (preg_match('/^\s*\(D\:(?P.*?)\)/s', $content, $match)) { + $name = $match['name']; + $name = str_replace("'", '', $name); + $date = false; + + // Smallest format : Y + // Full format : YmdHisP + if (preg_match('/^\d{4}(\d{2}(\d{2}(\d{2}(\d{2}(\d{2}(Z(\d{2,4})?|[\+-]?\d{2}(\d{2})?)?)?)?)?)?)?$/', $name)) { + if ($pos = strpos($name, 'Z')) { + $name = substr($name, 0, $pos + 1); + } elseif (18 == \strlen($name) && preg_match('/[^\+-]0000$/', $name)) { + $name = substr($name, 0, -4).'+0000'; + } + + $format = self::$formats[\strlen($name)]; + $date = \DateTime::createFromFormat($format, $name, new \DateTimeZone('UTC')); + } else { + // special cases + if (preg_match('/^\d{1,2}-\d{1,2}-\d{4},?\s+\d{2}:\d{2}:\d{2}[\+-]\d{4}$/', $name)) { + $name = str_replace(',', '', $name); + $format = 'n-j-Y H:i:sO'; + $date = \DateTime::createFromFormat($format, $name, new \DateTimeZone('UTC')); + } + } + + if (!$date) { + return false; + } + + $offset += strpos($content, '(D:') + \strlen($match['name']) + 4; // 1 for '(D:' and ')' + + return new self($date); + } + + return false; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementHexa.php b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementHexa.php new file mode 100644 index 0000000..e3265d2 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementHexa.php @@ -0,0 +1,85 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Element; + +use Smalot\PdfParser\Document; + +/** + * Class ElementHexa + */ +class ElementHexa extends ElementString +{ + /** + * @return bool|ElementHexa|ElementDate + */ + public static function parse(string $content, ?Document $document = null, int &$offset = 0) + { + if (preg_match('/^\s*\<(?P[A-F0-9]+)\>/is', $content, $match)) { + $name = $match['name']; + $offset += strpos($content, '<'.$name) + \strlen($name) + 2; // 1 for '>' + // repackage string as standard + $name = '('.self::decode($name).')'; + $element = ElementDate::parse($name, $document); + + if (!$element) { + $element = ElementString::parse($name, $document); + } + + return $element; + } + + return false; + } + + public static function decode(string $value): string + { + $text = ''; + $length = \strlen($value); + + if ('00' === substr($value, 0, 2)) { + for ($i = 0; $i < $length; $i += 4) { + $hex = substr($value, $i, 4); + $text .= '&#'.str_pad(hexdec($hex), 4, '0', \STR_PAD_LEFT).';'; + } + } else { + for ($i = 0; $i < $length; $i += 2) { + $hex = substr($value, $i, 2); + $text .= \chr(hexdec($hex)); + } + } + + $text = html_entity_decode($text, \ENT_NOQUOTES, 'UTF-8'); + + return $text; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementMissing.php b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementMissing.php new file mode 100644 index 0000000..d2fc000 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementMissing.php @@ -0,0 +1,66 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Element; + +use Smalot\PdfParser\Element; + +/** + * Class ElementMissing + */ +class ElementMissing extends Element +{ + public function __construct() + { + parent::__construct(null, null); + } + + public function equals($value): bool + { + return false; + } + + public function contains($value): bool + { + return false; + } + + public function getContent(): bool + { + return false; + } + + public function __toString(): string + { + return ''; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementName.php b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementName.php new file mode 100644 index 0000000..6e8d97a --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementName.php @@ -0,0 +1,69 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Element; + +use Smalot\PdfParser\Document; +use Smalot\PdfParser\Element; +use Smalot\PdfParser\Font; + +/** + * Class ElementName + */ +class ElementName extends Element +{ + public function __construct(string $value) + { + parent::__construct($value, null); + } + + public function equals($value): bool + { + return $value == $this->value; + } + + /** + * @return bool|ElementName + */ + public static function parse(string $content, ?Document $document = null, int &$offset = 0) + { + if (preg_match('/^\s*\/([A-Z0-9\-\+,#\.]+)/is', $content, $match)) { + $name = $match[1]; + $offset += strpos($content, $name) + \strlen($name); + $name = Font::decodeEntities($name); + + return new self($name); + } + + return false; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementNull.php b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementNull.php new file mode 100644 index 0000000..9af8843 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementNull.php @@ -0,0 +1,71 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Element; + +use Smalot\PdfParser\Document; +use Smalot\PdfParser\Element; + +/** + * Class ElementNull + */ +class ElementNull extends Element +{ + public function __construct() + { + parent::__construct(null, null); + } + + public function __toString(): string + { + return 'null'; + } + + public function equals($value): bool + { + return $this->getContent() === $value; + } + + /** + * @return bool|ElementNull + */ + public static function parse(string $content, ?Document $document = null, int &$offset = 0) + { + if (preg_match('/^\s*(null)/s', $content, $match)) { + $offset += strpos($content, 'null') + \strlen('null'); + + return new self(); + } + + return false; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementNumeric.php b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementNumeric.php new file mode 100644 index 0000000..5454acc --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementNumeric.php @@ -0,0 +1,62 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Element; + +use Smalot\PdfParser\Document; +use Smalot\PdfParser\Element; + +/** + * Class ElementNumeric + */ +class ElementNumeric extends Element +{ + public function __construct(string $value) + { + parent::__construct((float) $value, null); + } + + /** + * @return bool|ElementNumeric + */ + public static function parse(string $content, ?Document $document = null, int &$offset = 0) + { + if (preg_match('/^\s*(?P\-?[0-9\.]+)/s', $content, $match)) { + $value = $match['value']; + $offset += strpos($content, $value) + \strlen($value); + + return new self($value); + } + + return false; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementString.php b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementString.php new file mode 100644 index 0000000..011bcf4 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementString.php @@ -0,0 +1,93 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Element; + +use Smalot\PdfParser\Document; +use Smalot\PdfParser\Element; +use Smalot\PdfParser\Font; + +/** + * Class ElementString + */ +class ElementString extends Element +{ + public function __construct($value) + { + parent::__construct($value, null); + } + + public function equals($value): bool + { + return $value == $this->value; + } + + /** + * @return bool|ElementString + */ + public static function parse(string $content, ?Document $document = null, int &$offset = 0) + { + if (preg_match('/^\s*\((?P.*)/s', $content, $match)) { + $name = $match['name']; + + // Find next ')' not escaped. + $cur_start_text = $start_search_end = 0; + while (false !== ($cur_start_pos = strpos($name, ')', $start_search_end))) { + $cur_extract = substr($name, $cur_start_text, $cur_start_pos - $cur_start_text); + preg_match('/(?P[\\\]*)$/s', $cur_extract, $match); + if (!(\strlen($match['escape']) % 2)) { + break; + } + $start_search_end = $cur_start_pos + 1; + } + + // Extract string. + $name = substr($name, 0, (int) $cur_start_pos); + $offset += strpos($content, '(') + $cur_start_pos + 2; // 2 for '(' and ')' + $name = str_replace( + ['\\\\', '\\ ', '\\/', '\(', '\)', '\n', '\r', '\t'], + ['\\', ' ', '/', '(', ')', "\n", "\r", "\t"], + $name + ); + + // Decode string. + $name = Font::decodeOctal($name); + $name = Font::decodeEntities($name); + $name = Font::decodeHexadecimal($name, false); + $name = Font::decodeUnicode($name); + + return new self($name); + } + + return false; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementStruct.php b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementStruct.php new file mode 100644 index 0000000..c37b6da --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementStruct.php @@ -0,0 +1,75 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Element; + +use Smalot\PdfParser\Document; +use Smalot\PdfParser\Element; +use Smalot\PdfParser\Header; + +/** + * Class ElementStruct + */ +class ElementStruct extends Element +{ + /** + * @return false|Header + */ + public static function parse(string $content, ?Document $document = null, int &$offset = 0) + { + if (preg_match('/^\s*<<(?P.*)/is', $content)) { + preg_match_all('/(.*?)(<<|>>)/s', trim($content), $matches); + + $level = 0; + $sub = ''; + foreach ($matches[0] as $part) { + $sub .= $part; + $level += (false !== strpos($part, '<<') ? 1 : -1); + if ($level <= 0) { + break; + } + } + + $offset += strpos($content, '<<') + \strlen(rtrim($sub)); + + // Removes '<<' and '>>'. + $sub = trim((string) preg_replace('/^\s*<<(.*)>>\s*$/s', '\\1', $sub)); + + $position = 0; + $elements = Element::parse($sub, $document, $position); + + return new Header($elements, $document); + } + + return false; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementXRef.php b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementXRef.php new file mode 100644 index 0000000..ebba71a --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Element/ElementXRef.php @@ -0,0 +1,98 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Element; + +use Smalot\PdfParser\Document; +use Smalot\PdfParser\Element; + +/** + * Class ElementXRef + */ +class ElementXRef extends Element +{ + public function getId(): string + { + return $this->getContent(); + } + + public function getObject() + { + return $this->document->getObjectById($this->getId()); + } + + public function equals($value): bool + { + /** + * In case $value is a number and $this->value is a string like 5_0 + * + * Without this if-clause code like: + * + * $element = new ElementXRef('5_0'); + * $this->assertTrue($element->equals(5)); + * + * would fail (= 5_0 and 5 are not equal in PHP 8.0+). + */ + if ( + true === is_numeric($value) + && true === \is_string($this->getContent()) + && 1 === preg_match('/[0-9]+\_[0-9]+/', $this->getContent(), $matches) + ) { + return (float) $this->getContent() == $value; + } + + $id = ($value instanceof self) ? $value->getId() : $value; + + return $this->getId() == $id; + } + + public function __toString(): string + { + return '#Obj#'.$this->getId(); + } + + /** + * @return bool|ElementXRef + */ + public static function parse(string $content, ?Document $document = null, int &$offset = 0) + { + if (preg_match('/^\s*(?P[0-9]+\s+[0-9]+\s+R)/s', $content, $match)) { + $id = $match['id']; + $offset += strpos($content, $id) + \strlen($id); + $id = str_replace(' ', '_', rtrim($id, ' R')); + + return new self($id, $document); + } + + return false; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Encoding.php b/orcinus/pdfparser/src/Smalot/PdfParser/Encoding.php new file mode 100644 index 0000000..d5e4292 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Encoding.php @@ -0,0 +1,157 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser; + +use Exception; +use Smalot\PdfParser\Element\ElementNumeric; +use Smalot\PdfParser\Encoding\EncodingLocator; +use Smalot\PdfParser\Encoding\PostScriptGlyphs; +use Smalot\PdfParser\Exception\EncodingNotFoundException; + +/** + * Class Encoding + */ +class Encoding extends PDFObject +{ + /** + * @var array + */ + protected $encoding; + + /** + * @var array + */ + protected $differences; + + /** + * @var array + */ + protected $mapping; + + public function init() + { + $this->mapping = []; + $this->differences = []; + $this->encoding = []; + + if ($this->has('BaseEncoding')) { + $this->encoding = EncodingLocator::getEncoding($this->getEncodingClass())->getTranslations(); + + // Build table including differences. + $differences = $this->get('Differences')->getContent(); + $code = 0; + + if (!\is_array($differences)) { + return; + } + + foreach ($differences as $difference) { + /** @var ElementNumeric $difference */ + if ($difference instanceof ElementNumeric) { + $code = $difference->getContent(); + continue; + } + + // ElementName + $this->differences[$code] = $difference; + if (\is_object($difference)) { + $this->differences[$code] = $difference->getContent(); + } + + // For the next char. + ++$code; + } + + $this->mapping = $this->encoding; + foreach ($this->differences as $code => $difference) { + /* @var string $difference */ + $this->mapping[$code] = $difference; + } + } + } + + public function getDetails(bool $deep = true): array + { + $details = []; + + $details['BaseEncoding'] = ($this->has('BaseEncoding') ? (string) $this->get('BaseEncoding') : 'Ansi'); + $details['Differences'] = ($this->has('Differences') ? (string) $this->get('Differences') : ''); + + $details += parent::getDetails($deep); + + return $details; + } + + public function translateChar($dec): ?int + { + if (isset($this->mapping[$dec])) { + $dec = $this->mapping[$dec]; + } + + return PostScriptGlyphs::getCodePoint($dec); + } + + /** + * Returns encoding class name if available or empty string (only prior PHP 7.4). + * + * @throws \Exception On PHP 7.4+ an exception is thrown if encoding class doesn't exist. + */ + public function __toString(): string + { + try { + return $this->getEncodingClass(); + } catch (\Exception $e) { + // prior to PHP 7.4 toString has to return an empty string. + if (version_compare(\PHP_VERSION, '7.4.0', '<')) { + return ''; + } + throw $e; + } + } + + /** + * @throws EncodingNotFoundException + */ + protected function getEncodingClass(): string + { + // Load reference table charset. + $baseEncoding = preg_replace('/[^A-Z0-9]/is', '', $this->get('BaseEncoding')->getContent()); + $className = '\\Smalot\\PdfParser\\Encoding\\'.$baseEncoding; + + if (!class_exists($className)) { + throw new EncodingNotFoundException('Missing encoding data for: "'.$baseEncoding.'".'); + } + + return $className; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/AbstractEncoding.php b/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/AbstractEncoding.php new file mode 100644 index 0000000..aea9c02 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/AbstractEncoding.php @@ -0,0 +1,8 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +// Source : http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/ISOLatin1Encoding.pm + +namespace Smalot\PdfParser\Encoding; + +/** + * Class ISOLatin1Encoding + */ +class ISOLatin1Encoding extends AbstractEncoding +{ + public function getTranslations(): array + { + $encoding = + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + 'space exclam quotedbl numbersign dollar percent ampersand quoteright '. + 'parenleft parenright asterisk plus comma minus period slash zero one '. + 'two three four five six seven eight nine colon semicolon less equal '. + 'greater question at A B C D E F G H I J K L M N O P Q R S T U V W X '. + 'Y Z bracketleft backslash bracketright asciicircum underscore '. + 'quoteleft a b c d e f g h i j k l m n o p q r s t u v w x y z '. + 'braceleft bar braceright asciitilde .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef dotlessi grave acute '. + 'circumflex tilde macron breve dotaccent dieresis .notdef ring '. + 'cedilla .notdef hungarumlaut ogonek caron space exclamdown cent '. + 'sterling currency yen brokenbar section dieresis copyright '. + 'ordfeminine guillemotleft logicalnot hyphen registered macron degree '. + 'plusminus twosuperior threesuperior acute mu paragraph '. + 'periodcentered cedilla onesuperior ordmasculine guillemotright '. + 'onequarter onehalf threequarters questiondown Agrave Aacute '. + 'Acircumflex Atilde Adieresis Aring AE Ccedilla Egrave Eacute '. + 'Ecircumflex Edieresis Igrave Iacute Icircumflex Idieresis Eth Ntilde '. + 'Ograve Oacute Ocircumflex Otilde Odieresis multiply Oslash Ugrave '. + 'Uacute Ucircumflex Udieresis Yacute Thorn germandbls agrave aacute '. + 'acircumflex atilde adieresis aring ae ccedilla egrave eacute '. + 'ecircumflex edieresis igrave iacute icircumflex idieresis eth ntilde '. + 'ograve oacute ocircumflex otilde odieresis divide oslash ugrave '. + 'uacute ucircumflex udieresis yacute thorn ydieresis'; + + return explode(' ', $encoding); + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/ISOLatin9Encoding.php b/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/ISOLatin9Encoding.php new file mode 100644 index 0000000..616a0f5 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/ISOLatin9Encoding.php @@ -0,0 +1,76 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +// Source : http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/ISOLatin9Encoding.pm + +namespace Smalot\PdfParser\Encoding; + +/** + * Class ISOLatin9Encoding + */ +class ISOLatin9Encoding extends AbstractEncoding +{ + public function getTranslations(): array + { + $encoding = + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + 'space exclam quotedbl numbersign dollar percent ampersand quoteright '. + 'parenleft parenright asterisk plus comma minus period slash zero one '. + 'two three four five six seven eight nine colon semicolon less equal '. + 'greater question at A B C D E F G H I J K L M N O P Q R S T U V W X '. + 'Y Z bracketleft backslash bracketright asciicircum underscore '. + 'quoteleft a b c d e f g h i j k l m n o p q r s t u v w x y z '. + 'braceleft bar braceright asciitilde .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef dotlessi grave acute '. + 'circumflex tilde macron breve dotaccent dieresis .notdef ring '. + 'cedilla .notdef hungarumlaut ogonek caron space exclamdown cent '. + 'sterling Euro yen Scaron section scaron copyright '. + 'ordfeminine guillemotleft logicalnot hyphen registered macron degree '. + 'plusminus twosuperior threesuperior Zcaron mu paragraph '. + 'periodcentered zcaron onesuperior ordmasculine guillemotright '. + 'OE oe Ydieresis questiondown Agrave Aacute '. + 'Acircumflex Atilde Adieresis Aring AE Ccedilla Egrave Eacute '. + 'Ecircumflex Edieresis Igrave Iacute Icircumflex Idieresis Eth Ntilde '. + 'Ograve Oacute Ocircumflex Otilde Odieresis multiply Oslash Ugrave '. + 'Uacute Ucircumflex Udieresis Yacute Thorn germandbls agrave aacute '. + 'acircumflex atilde adieresis aring ae ccedilla egrave eacute '. + 'ecircumflex edieresis igrave iacute icircumflex idieresis eth ntilde '. + 'ograve oacute ocircumflex otilde odieresis divide oslash ugrave '. + 'uacute ucircumflex udieresis yacute thorn ydieresis'; + + return explode(' ', $encoding); + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/MacRomanEncoding.php b/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/MacRomanEncoding.php new file mode 100644 index 0000000..c47131c --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/MacRomanEncoding.php @@ -0,0 +1,80 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +// Source : http://www.opensource.apple.com/source/vim/vim-34/vim/runtime/print/mac-roman.ps + +namespace Smalot\PdfParser\Encoding; + +/** + * Class MacRomanEncoding + */ +class MacRomanEncoding extends AbstractEncoding +{ + public function getTranslations(): array + { + $encoding = + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + 'space exclam quotedbl numbersign dollar percent ampersand quotesingle '. + 'parenleft parenright asterisk plus comma minus period slash '. + 'zero one two three four five six seven '. + 'eight nine colon semicolon less equal greater question '. + 'at A B C D E F G '. + 'H I J K L M N O '. + 'P Q R S T U V W '. + 'X Y Z bracketleft backslash bracketright asciicircum underscore '. + 'grave a b c d e f g '. + 'h i j k l m n o '. + 'p q r s t u v w '. + 'x y z braceleft bar braceright asciitilde .notdef '. + 'Adieresis Aring Ccedilla Eacute Ntilde Odieresis Udieresis aacute '. + 'agrave acircumflex adieresis atilde aring ccedilla eacute egrave '. + 'ecircumflex edieresis iacute igrave icircumflex idieresis ntilde oacute '. + 'ograve ocircumflex odieresis otilde uacute ugrave ucircumflex udieresis '. + 'dagger degree cent sterling section bullet paragraph germandbls '. + 'registered copyright trademark acute dieresis notequal AE Oslash '. + 'infinity plusminus lessequal greaterequal yen mu partialdiff summation '. + 'Pi pi integral ordfeminine ordmasculine Omega ae oslash '. + 'questiondown exclamdown logicalnot radical florin approxequal delta guillemotleft '. + 'guillemotright ellipsis space Agrave Atilde Otilde OE oe '. + 'endash emdash quotedblleft quotedblright quoteleft quoteright divide lozenge '. + 'ydieresis Ydieresis fraction currency guilsinglleft guilsinglright fi fl '. + 'daggerdbl periodcentered quotesinglbase quotedblbase perthousand Acircumflex Ecircumflex Aacute '. + 'Edieresis Egrave Iacute Icircumflex Idieresis Igrave Oacute Ocircumflex '. + 'heart Ograve Uacute Ucircumflex Ugrave dotlessi circumflex tilde '. + 'macron breve dotaccent ring cedilla hungarumlaut ogonek caron'; + + return explode(' ', $encoding); + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/PostScriptGlyphs.php b/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/PostScriptGlyphs.php new file mode 100644 index 0000000..fbe1af4 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/PostScriptGlyphs.php @@ -0,0 +1,1099 @@ + + * + * @date 2019-09-17 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Encoding; + +/** + * Class PostScriptGlyphs + */ +class PostScriptGlyphs +{ + /** + * The mapping tables have been converted from https://github.com/OpenPrinting/cups-filters/blob/master/fontembed/aglfn13.c, + * part of the OpenPrinting/cups-filters package, which itself is licensed under the MIT license and lists this specific code part as: + * Copyright 2008,2012 Tobias Hoffmann under the Expat license (https://www.gnu.org/licenses/license-list.html#Expat) + */ + public static function getGlyphs(): array + { + return [ + 'space' => '0x00a0', + 'exclam' => '0x0021', + 'quotedbl' => '0x0022', + 'numbersign' => '0x0023', + 'dollar' => '0x0024', + 'percent' => '0x0025', + 'ampersand' => '0x0026', + 'quotesingle' => '0x0027', + 'parenleft' => '0x0028', + 'parenright' => '0x0029', + 'asterisk' => '0x002a', + 'plus' => '0x002b', + 'comma' => '0x002c', + 'hyphen' => '0x002d', + 'period' => '0x002e', + 'slash' => '0x002f', + 'zero' => '0x0030', + 'one' => '0x0031', + 'two' => '0x0032', + 'three' => '0x0033', + 'four' => '0x0034', + 'five' => '0x0035', + 'six' => '0x0036', + 'seven' => '0x0037', + 'eight' => '0x0038', + 'nine' => '0x0039', + 'colon' => '0x003a', + 'semicolon' => '0x003b', + 'less' => '0x003c', + 'equal' => '0x003d', + 'greater' => '0x003e', + 'question' => '0x003f', + 'at' => '0x0040', + 'A' => '0x0041', + 'B' => '0x0042', + 'C' => '0x0043', + 'D' => '0x0044', + 'E' => '0x0045', + 'F' => '0x0046', + 'G' => '0x0047', + 'H' => '0x0048', + 'I' => '0x0049', + 'J' => '0x004a', + 'K' => '0x004b', + 'L' => '0x004c', + 'M' => '0x004d', + 'N' => '0x004e', + 'O' => '0x004f', + 'P' => '0x0050', + 'Q' => '0x0051', + 'R' => '0x0052', + 'S' => '0x0053', + 'T' => '0x0054', + 'U' => '0x0055', + 'V' => '0x0056', + 'W' => '0x0057', + 'X' => '0x0058', + 'Y' => '0x0059', + 'Z' => '0x005a', + 'bracketleft' => '0x005b', + 'backslash' => '0x005c', + 'bracketright' => '0x005d', + 'asciicircum' => '0x005e', + 'underscore' => '0x005f', + 'grave' => '0x0060', + 'a' => '0x0061', + 'b' => '0x0062', + 'c' => '0x0063', + 'd' => '0x0064', + 'e' => '0x0065', + 'f' => '0x0066', + 'g' => '0x0067', + 'h' => '0x0068', + 'i' => '0x0069', + 'j' => '0x006a', + 'k' => '0x006b', + 'l' => '0x006c', + 'm' => '0x006d', + 'n' => '0x006e', + 'o' => '0x006f', + 'p' => '0x0070', + 'q' => '0x0071', + 'r' => '0x0072', + 's' => '0x0073', + 't' => '0x0074', + 'u' => '0x0075', + 'v' => '0x0076', + 'w' => '0x0077', + 'x' => '0x0078', + 'y' => '0x0079', + 'z' => '0x007a', + 'braceleft' => '0x007b', + 'bar' => '0x007c', + 'braceright' => '0x007d', + 'asciitilde' => '0x007e', + 'exclamdown' => '0x00a1', + 'cent' => '0x00a2', + 'sterling' => '0x00a3', + 'currency' => '0x00a4', + 'yen' => '0x00a5', + 'brokenbar' => '0x00a6', + 'section' => '0x00a7', + 'dieresis' => '0x00a8', + 'copyright' => '0x00a9', + 'ordfeminine' => '0x00aa', + 'guillemotleft' => '0x00ab', + 'logicalnot' => '0x00ac', + 'minus' => '0x2212', + 'registered' => '0x00ae', + 'macron' => '0x02c9', + 'degree' => '0x00b0', + 'plusminus' => '0x00b1', + 'twosuperior' => '0x00b2', + 'threesuperior' => '0x00b3', + 'acute' => '0x00b4', + 'mu' => '0x03bc', + 'paragraph' => '0x00b6', + 'periodcentered' => '0x2219', + 'cedilla' => '0x00b8', + 'onesuperior' => '0x00b9', + 'ordmasculine' => '0x00ba', + 'guillemotright' => '0x00bb', + 'onequarter' => '0x00bc', + 'onehalf' => '0x00bd', + 'threequarters' => '0x00be', + 'questiondown' => '0x00bf', + 'Agrave' => '0x00c0', + 'Aacute' => '0x00c1', + 'Acircumflex' => '0x00c2', + 'Atilde' => '0x00c3', + 'Adieresis' => '0x00c4', + 'Aring' => '0x00c5', + 'AE' => '0x00c6', + 'Ccedilla' => '0x00c7', + 'Egrave' => '0x00c8', + 'Eacute' => '0x00c9', + 'Ecircumflex' => '0x00ca', + 'Edieresis' => '0x00cb', + 'Igrave' => '0x00cc', + 'Iacute' => '0x00cd', + 'Icircumflex' => '0x00ce', + 'Idieresis' => '0x00cf', + 'Eth' => '0x00d0', + 'Ntilde' => '0x00d1', + 'Ograve' => '0x00d2', + 'Oacute' => '0x00d3', + 'Ocircumflex' => '0x00d4', + 'Otilde' => '0x00d5', + 'Odieresis' => '0x00d6', + 'multiply' => '0x00d7', + 'Oslash' => '0x00d8', + 'Ugrave' => '0x00d9', + 'Uacute' => '0x00da', + 'Ucircumflex' => '0x00db', + 'Udieresis' => '0x00dc', + 'Yacute' => '0x00dd', + 'Thorn' => '0x00de', + 'germandbls' => '0x00df', + 'agrave' => '0x00e0', + 'aacute' => '0x00e1', + 'acircumflex' => '0x00e2', + 'atilde' => '0x00e3', + 'adieresis' => '0x00e4', + 'aring' => '0x00e5', + 'ae' => '0x00e6', + 'ccedilla' => '0x00e7', + 'egrave' => '0x00e8', + 'eacute' => '0x00e9', + 'ecircumflex' => '0x00ea', + 'edieresis' => '0x00eb', + 'igrave' => '0x00ec', + 'iacute' => '0x00ed', + 'icircumflex' => '0x00ee', + 'idieresis' => '0x00ef', + 'eth' => '0x00f0', + 'ntilde' => '0x00f1', + 'ograve' => '0x00f2', + 'oacute' => '0x00f3', + 'ocircumflex' => '0x00f4', + 'otilde' => '0x00f5', + 'odieresis' => '0x00f6', + 'divide' => '0x00f7', + 'oslash' => '0x00f8', + 'ugrave' => '0x00f9', + 'uacute' => '0x00fa', + 'ucircumflex' => '0x00fb', + 'udieresis' => '0x00fc', + 'yacute' => '0x00fd', + 'thorn' => '0x00fe', + 'ydieresis' => '0x00ff', + 'Amacron' => '0x0100', + 'amacron' => '0x0101', + 'Abreve' => '0x0102', + 'abreve' => '0x0103', + 'Aogonek' => '0x0104', + 'aogonek' => '0x0105', + 'Cacute' => '0x0106', + 'cacute' => '0x0107', + 'Ccircumflex' => '0x0108', + 'ccircumflex' => '0x0109', + 'Cdotaccent' => '0x010a', + 'cdotaccent' => '0x010b', + 'Ccaron' => '0x010c', + 'ccaron' => '0x010d', + 'Dcaron' => '0x010e', + 'dcaron' => '0x010f', + 'Dcroat' => '0x0110', + 'dcroat' => '0x0111', + 'Emacron' => '0x0112', + 'emacron' => '0x0113', + 'Ebreve' => '0x0114', + 'ebreve' => '0x0115', + 'Edotaccent' => '0x0116', + 'edotaccent' => '0x0117', + 'Eogonek' => '0x0118', + 'eogonek' => '0x0119', + 'Ecaron' => '0x011a', + 'ecaron' => '0x011b', + 'Gcircumflex' => '0x011c', + 'gcircumflex' => '0x011d', + 'Gbreve' => '0x011e', + 'gbreve' => '0x011f', + 'Gdotaccent' => '0x0120', + 'gdotaccent' => '0x0121', + 'Gcommaaccent' => '0x0122', + 'gcommaaccent' => '0x0123', + 'Hcircumflex' => '0x0124', + 'hcircumflex' => '0x0125', + 'Hbar' => '0x0126', + 'hbar' => '0x0127', + 'Itilde' => '0x0128', + 'itilde' => '0x0129', + 'Imacron' => '0x012a', + 'imacron' => '0x012b', + 'Ibreve' => '0x012c', + 'ibreve' => '0x012d', + 'Iogonek' => '0x012e', + 'iogonek' => '0x012f', + 'Idotaccent' => '0x0130', + 'dotlessi' => '0x0131', + 'IJ' => '0x0132', + 'ij' => '0x0133', + 'Jcircumflex' => '0x0134', + 'jcircumflex' => '0x0135', + 'Kcommaaccent' => '0x0136', + 'kcommaaccent' => '0x0137', + 'kgreenlandic' => '0x0138', + 'Lacute' => '0x0139', + 'lacute' => '0x013a', + 'Lcommaaccent' => '0x013b', + 'lcommaaccent' => '0x013c', + 'Lcaron' => '0x013d', + 'lcaron' => '0x013e', + 'Ldot' => '0x013f', + 'ldot' => '0x0140', + 'Lslash' => '0x0141', + 'lslash' => '0x0142', + 'Nacute' => '0x0143', + 'nacute' => '0x0144', + 'Ncommaaccent' => '0x0145', + 'ncommaaccent' => '0x0146', + 'Ncaron' => '0x0147', + 'ncaron' => '0x0148', + 'napostrophe' => '0x0149', + 'Eng' => '0x014a', + 'eng' => '0x014b', + 'Omacron' => '0x014c', + 'omacron' => '0x014d', + 'Obreve' => '0x014e', + 'obreve' => '0x014f', + 'Ohungarumlaut' => '0x0150', + 'ohungarumlaut' => '0x0151', + 'OE' => '0x0152', + 'oe' => '0x0153', + 'Racute' => '0x0154', + 'racute' => '0x0155', + 'Rcommaaccent' => '0x0156', + 'rcommaaccent' => '0x0157', + 'Rcaron' => '0x0158', + 'rcaron' => '0x0159', + 'Sacute' => '0x015a', + 'sacute' => '0x015b', + 'Scircumflex' => '0x015c', + 'scircumflex' => '0x015d', + 'Scedilla' => '0xf6c1', + 'scedilla' => '0xf6c2', + 'Scaron' => '0x0160', + 'scaron' => '0x0161', + 'Tcommaaccent' => '0x021a', + 'tcommaaccent' => '0x021b', + 'Tcaron' => '0x0164', + 'tcaron' => '0x0165', + 'Tbar' => '0x0166', + 'tbar' => '0x0167', + 'Utilde' => '0x0168', + 'utilde' => '0x0169', + 'Umacron' => '0x016a', + 'umacron' => '0x016b', + 'Ubreve' => '0x016c', + 'ubreve' => '0x016d', + 'Uring' => '0x016e', + 'uring' => '0x016f', + 'Uhungarumlaut' => '0x0170', + 'uhungarumlaut' => '0x0171', + 'Uogonek' => '0x0172', + 'uogonek' => '0x0173', + 'Wcircumflex' => '0x0174', + 'wcircumflex' => '0x0175', + 'Ycircumflex' => '0x0176', + 'ycircumflex' => '0x0177', + 'Ydieresis' => '0x0178', + 'Zacute' => '0x0179', + 'zacute' => '0x017a', + 'Zdotaccent' => '0x017b', + 'zdotaccent' => '0x017c', + 'Zcaron' => '0x017d', + 'zcaron' => '0x017e', + 'longs' => '0x017f', + 'florin' => '0x0192', + 'Ohorn' => '0x01a0', + 'ohorn' => '0x01a1', + 'Uhorn' => '0x01af', + 'uhorn' => '0x01b0', + 'Gcaron' => '0x01e6', + 'gcaron' => '0x01e7', + 'Aringacute' => '0x01fa', + 'aringacute' => '0x01fb', + 'AEacute' => '0x01fc', + 'aeacute' => '0x01fd', + 'Oslashacute' => '0x01fe', + 'oslashacute' => '0x01ff', + 'Scommaaccent' => '0x0218', + 'scommaaccent' => '0x0219', + 'afii57929' => '0x02bc', + 'afii64937' => '0x02bd', + 'circumflex' => '0x02c6', + 'caron' => '0x02c7', + 'breve' => '0x02d8', + 'dotaccent' => '0x02d9', + 'ring' => '0x02da', + 'ogonek' => '0x02db', + 'tilde' => '0x02dc', + 'hungarumlaut' => '0x02dd', + 'gravecomb' => '0x0300', + 'acutecomb' => '0x0301', + 'tildecomb' => '0x0303', + 'hookabovecomb' => '0x0309', + 'dotbelowcomb' => '0x0323', + 'tonos' => '0x0384', + 'dieresistonos' => '0x0385', + 'Alphatonos' => '0x0386', + 'anoteleia' => '0x0387', + 'Epsilontonos' => '0x0388', + 'Etatonos' => '0x0389', + 'Iotatonos' => '0x038a', + 'Omicrontonos' => '0x038c', + 'Upsilontonos' => '0x038e', + 'Omegatonos' => '0x038f', + 'iotadieresistonos' => '0x0390', + 'Alpha' => '0x0391', + 'Beta' => '0x0392', + 'Gamma' => '0x0393', + 'Delta' => '0x2206', + 'Epsilon' => '0x0395', + 'Zeta' => '0x0396', + 'Eta' => '0x0397', + 'Theta' => '0x0398', + 'Iota' => '0x0399', + 'Kappa' => '0x039a', + 'Lambda' => '0x039b', + 'Mu' => '0x039c', + 'Nu' => '0x039d', + 'Xi' => '0x039e', + 'Omicron' => '0x039f', + 'Pi' => '0x03a0', + 'Rho' => '0x03a1', + 'Sigma' => '0x03a3', + 'Tau' => '0x03a4', + 'Upsilon' => '0x03a5', + 'Phi' => '0x03a6', + 'Chi' => '0x03a7', + 'Psi' => '0x03a8', + 'Omega' => '0x2126', + 'Iotadieresis' => '0x03aa', + 'Upsilondieresis' => '0x03ab', + 'alphatonos' => '0x03ac', + 'epsilontonos' => '0x03ad', + 'etatonos' => '0x03ae', + 'iotatonos' => '0x03af', + 'upsilondieresistonos' => '0x03b0', + 'alpha' => '0x03b1', + 'beta' => '0x03b2', + 'gamma' => '0x03b3', + 'delta' => '0x03b4', + 'epsilon' => '0x03b5', + 'zeta' => '0x03b6', + 'eta' => '0x03b7', + 'theta' => '0x03b8', + 'iota' => '0x03b9', + 'kappa' => '0x03ba', + 'lambda' => '0x03bb', + 'nu' => '0x03bd', + 'xi' => '0x03be', + 'omicron' => '0x03bf', + 'pi' => '0x03c0', + 'rho' => '0x03c1', + 'sigma1' => '0x03c2', + 'sigma' => '0x03c3', + 'tau' => '0x03c4', + 'upsilon' => '0x03c5', + 'phi' => '0x03c6', + 'chi' => '0x03c7', + 'psi' => '0x03c8', + 'omega' => '0x03c9', + 'iotadieresis' => '0x03ca', + 'upsilondieresis' => '0x03cb', + 'omicrontonos' => '0x03cc', + 'upsilontonos' => '0x03cd', + 'omegatonos' => '0x03ce', + 'theta1' => '0x03d1', + 'Upsilon1' => '0x03d2', + 'phi1' => '0x03d5', + 'omega1' => '0x03d6', + 'afii10023' => '0x0401', + 'afii10051' => '0x0402', + 'afii10052' => '0x0403', + 'afii10053' => '0x0404', + 'afii10054' => '0x0405', + 'afii10055' => '0x0406', + 'afii10056' => '0x0407', + 'afii10057' => '0x0408', + 'afii10058' => '0x0409', + 'afii10059' => '0x040a', + 'afii10060' => '0x040b', + 'afii10061' => '0x040c', + 'afii10062' => '0x040e', + 'afii10145' => '0x040f', + 'afii10017' => '0x0410', + 'afii10018' => '0x0411', + 'afii10019' => '0x0412', + 'afii10020' => '0x0413', + 'afii10021' => '0x0414', + 'afii10022' => '0x0415', + 'afii10024' => '0x0416', + 'afii10025' => '0x0417', + 'afii10026' => '0x0418', + 'afii10027' => '0x0419', + 'afii10028' => '0x041a', + 'afii10029' => '0x041b', + 'afii10030' => '0x041c', + 'afii10031' => '0x041d', + 'afii10032' => '0x041e', + 'afii10033' => '0x041f', + 'afii10034' => '0x0420', + 'afii10035' => '0x0421', + 'afii10036' => '0x0422', + 'afii10037' => '0x0423', + 'afii10038' => '0x0424', + 'afii10039' => '0x0425', + 'afii10040' => '0x0426', + 'afii10041' => '0x0427', + 'afii10042' => '0x0428', + 'afii10043' => '0x0429', + 'afii10044' => '0x042a', + 'afii10045' => '0x042b', + 'afii10046' => '0x042c', + 'afii10047' => '0x042d', + 'afii10048' => '0x042e', + 'afii10049' => '0x042f', + 'afii10065' => '0x0430', + 'afii10066' => '0x0431', + 'afii10067' => '0x0432', + 'afii10068' => '0x0433', + 'afii10069' => '0x0434', + 'afii10070' => '0x0435', + 'afii10072' => '0x0436', + 'afii10073' => '0x0437', + 'afii10074' => '0x0438', + 'afii10075' => '0x0439', + 'afii10076' => '0x043a', + 'afii10077' => '0x043b', + 'afii10078' => '0x043c', + 'afii10079' => '0x043d', + 'afii10080' => '0x043e', + 'afii10081' => '0x043f', + 'afii10082' => '0x0440', + 'afii10083' => '0x0441', + 'afii10084' => '0x0442', + 'afii10085' => '0x0443', + 'afii10086' => '0x0444', + 'afii10087' => '0x0445', + 'afii10088' => '0x0446', + 'afii10089' => '0x0447', + 'afii10090' => '0x0448', + 'afii10091' => '0x0449', + 'afii10092' => '0x044a', + 'afii10093' => '0x044b', + 'afii10094' => '0x044c', + 'afii10095' => '0x044d', + 'afii10096' => '0x044e', + 'afii10097' => '0x044f', + 'afii10071' => '0x0451', + 'afii10099' => '0x0452', + 'afii10100' => '0x0453', + 'afii10101' => '0x0454', + 'afii10102' => '0x0455', + 'afii10103' => '0x0456', + 'afii10104' => '0x0457', + 'afii10105' => '0x0458', + 'afii10106' => '0x0459', + 'afii10107' => '0x045a', + 'afii10108' => '0x045b', + 'afii10109' => '0x045c', + 'afii10110' => '0x045e', + 'afii10193' => '0x045f', + 'afii10146' => '0x0462', + 'afii10194' => '0x0463', + 'afii10147' => '0x0472', + 'afii10195' => '0x0473', + 'afii10148' => '0x0474', + 'afii10196' => '0x0475', + 'afii10050' => '0x0490', + 'afii10098' => '0x0491', + 'afii10846' => '0x04d9', + 'afii57799' => '0x05b0', + 'afii57801' => '0x05b1', + 'afii57800' => '0x05b2', + 'afii57802' => '0x05b3', + 'afii57793' => '0x05b4', + 'afii57794' => '0x05b5', + 'afii57795' => '0x05b6', + 'afii57798' => '0x05b7', + 'afii57797' => '0x05b8', + 'afii57806' => '0x05b9', + 'afii57796' => '0x05bb', + 'afii57807' => '0x05bc', + 'afii57839' => '0x05bd', + 'afii57645' => '0x05be', + 'afii57841' => '0x05bf', + 'afii57842' => '0x05c0', + 'afii57804' => '0x05c1', + 'afii57803' => '0x05c2', + 'afii57658' => '0x05c3', + 'afii57664' => '0x05d0', + 'afii57665' => '0x05d1', + 'afii57666' => '0x05d2', + 'afii57667' => '0x05d3', + 'afii57668' => '0x05d4', + 'afii57669' => '0x05d5', + 'afii57670' => '0x05d6', + 'afii57671' => '0x05d7', + 'afii57672' => '0x05d8', + 'afii57673' => '0x05d9', + 'afii57674' => '0x05da', + 'afii57675' => '0x05db', + 'afii57676' => '0x05dc', + 'afii57677' => '0x05dd', + 'afii57678' => '0x05de', + 'afii57679' => '0x05df', + 'afii57680' => '0x05e0', + 'afii57681' => '0x05e1', + 'afii57682' => '0x05e2', + 'afii57683' => '0x05e3', + 'afii57684' => '0x05e4', + 'afii57685' => '0x05e5', + 'afii57686' => '0x05e6', + 'afii57687' => '0x05e7', + 'afii57688' => '0x05e8', + 'afii57689' => '0x05e9', + 'afii57690' => '0x05ea', + 'afii57716' => '0x05f0', + 'afii57717' => '0x05f1', + 'afii57718' => '0x05f2', + 'afii57388' => '0x060c', + 'afii57403' => '0x061b', + 'afii57407' => '0x061f', + 'afii57409' => '0x0621', + 'afii57410' => '0x0622', + 'afii57411' => '0x0623', + 'afii57412' => '0x0624', + 'afii57413' => '0x0625', + 'afii57414' => '0x0626', + 'afii57415' => '0x0627', + 'afii57416' => '0x0628', + 'afii57417' => '0x0629', + 'afii57418' => '0x062a', + 'afii57419' => '0x062b', + 'afii57420' => '0x062c', + 'afii57421' => '0x062d', + 'afii57422' => '0x062e', + 'afii57423' => '0x062f', + 'afii57424' => '0x0630', + 'afii57425' => '0x0631', + 'afii57426' => '0x0632', + 'afii57427' => '0x0633', + 'afii57428' => '0x0634', + 'afii57429' => '0x0635', + 'afii57430' => '0x0636', + 'afii57431' => '0x0637', + 'afii57432' => '0x0638', + 'afii57433' => '0x0639', + 'afii57434' => '0x063a', + 'afii57440' => '0x0640', + 'afii57441' => '0x0641', + 'afii57442' => '0x0642', + 'afii57443' => '0x0643', + 'afii57444' => '0x0644', + 'afii57445' => '0x0645', + 'afii57446' => '0x0646', + 'afii57470' => '0x0647', + 'afii57448' => '0x0648', + 'afii57449' => '0x0649', + 'afii57450' => '0x064a', + 'afii57451' => '0x064b', + 'afii57452' => '0x064c', + 'afii57453' => '0x064d', + 'afii57454' => '0x064e', + 'afii57455' => '0x064f', + 'afii57456' => '0x0650', + 'afii57457' => '0x0651', + 'afii57458' => '0x0652', + 'afii57392' => '0x0660', + 'afii57393' => '0x0661', + 'afii57394' => '0x0662', + 'afii57395' => '0x0663', + 'afii57396' => '0x0664', + 'afii57397' => '0x0665', + 'afii57398' => '0x0666', + 'afii57399' => '0x0667', + 'afii57400' => '0x0668', + 'afii57401' => '0x0669', + 'afii57381' => '0x066a', + 'afii63167' => '0x066d', + 'afii57511' => '0x0679', + 'afii57506' => '0x067e', + 'afii57507' => '0x0686', + 'afii57512' => '0x0688', + 'afii57513' => '0x0691', + 'afii57508' => '0x0698', + 'afii57505' => '0x06a4', + 'afii57509' => '0x06af', + 'afii57514' => '0x06ba', + 'afii57519' => '0x06d2', + 'afii57534' => '0x06d5', + 'Wgrave' => '0x1e80', + 'wgrave' => '0x1e81', + 'Wacute' => '0x1e82', + 'wacute' => '0x1e83', + 'Wdieresis' => '0x1e84', + 'wdieresis' => '0x1e85', + 'Ygrave' => '0x1ef2', + 'ygrave' => '0x1ef3', + 'afii61664' => '0x200c', + 'afii301' => '0x200d', + 'afii299' => '0x200e', + 'afii300' => '0x200f', + 'figuredash' => '0x2012', + 'endash' => '0x2013', + 'emdash' => '0x2014', + 'afii00208' => '0x2015', + 'underscoredbl' => '0x2017', + 'quoteleft' => '0x2018', + 'quoteright' => '0x2019', + 'quotesinglbase' => '0x201a', + 'quotereversed' => '0x201b', + 'quotedblleft' => '0x201c', + 'quotedblright' => '0x201d', + 'quotedblbase' => '0x201e', + 'dagger' => '0x2020', + 'daggerdbl' => '0x2021', + 'bullet' => '0x2022', + 'onedotenleader' => '0x2024', + 'twodotenleader' => '0x2025', + 'ellipsis' => '0x2026', + 'afii61573' => '0x202c', + 'afii61574' => '0x202d', + 'afii61575' => '0x202e', + 'perthousand' => '0x2030', + 'minute' => '0x2032', + 'second' => '0x2033', + 'guilsinglleft' => '0x2039', + 'guilsinglright' => '0x203a', + 'exclamdbl' => '0x203c', + 'fraction' => '0x2215', + 'zerosuperior' => '0x2070', + 'foursuperior' => '0x2074', + 'fivesuperior' => '0x2075', + 'sixsuperior' => '0x2076', + 'sevensuperior' => '0x2077', + 'eightsuperior' => '0x2078', + 'ninesuperior' => '0x2079', + 'parenleftsuperior' => '0x207d', + 'parenrightsuperior' => '0x207e', + 'nsuperior' => '0x207f', + 'zeroinferior' => '0x2080', + 'oneinferior' => '0x2081', + 'twoinferior' => '0x2082', + 'threeinferior' => '0x2083', + 'fourinferior' => '0x2084', + 'fiveinferior' => '0x2085', + 'sixinferior' => '0x2086', + 'seveninferior' => '0x2087', + 'eightinferior' => '0x2088', + 'nineinferior' => '0x2089', + 'parenleftinferior' => '0x208d', + 'parenrightinferior' => '0x208e', + 'colonmonetary' => '0x20a1', + 'franc' => '0x20a3', + 'lira' => '0x20a4', + 'peseta' => '0x20a7', + 'afii57636' => '0x20aa', + 'dong' => '0x20ab', + 'Euro' => '0x20ac', + 'afii61248' => '0x2105', + 'Ifraktur' => '0x2111', + 'afii61289' => '0x2113', + 'afii61352' => '0x2116', + 'weierstrass' => '0x2118', + 'Rfraktur' => '0x211c', + 'prescription' => '0x211e', + 'trademark' => '0x2122', + 'estimated' => '0x212e', + 'aleph' => '0x2135', + 'onethird' => '0x2153', + 'twothirds' => '0x2154', + 'oneeighth' => '0x215b', + 'threeeighths' => '0x215c', + 'fiveeighths' => '0x215d', + 'seveneighths' => '0x215e', + 'arrowleft' => '0x2190', + 'arrowup' => '0x2191', + 'arrowright' => '0x2192', + 'arrowdown' => '0x2193', + 'arrowboth' => '0x2194', + 'arrowupdn' => '0x2195', + 'arrowupdnbse' => '0x21a8', + 'carriagereturn' => '0x21b5', + 'arrowdblleft' => '0x21d0', + 'arrowdblup' => '0x21d1', + 'arrowdblright' => '0x21d2', + 'arrowdbldown' => '0x21d3', + 'arrowdblboth' => '0x21d4', + 'universal' => '0x2200', + 'partialdiff' => '0x2202', + 'existential' => '0x2203', + 'emptyset' => '0x2205', + 'gradient' => '0x2207', + 'element' => '0x2208', + 'notelement' => '0x2209', + 'suchthat' => '0x220b', + 'product' => '0x220f', + 'summation' => '0x2211', + 'asteriskmath' => '0x2217', + 'radical' => '0x221a', + 'proportional' => '0x221d', + 'infinity' => '0x221e', + 'orthogonal' => '0x221f', + 'angle' => '0x2220', + 'logicaland' => '0x2227', + 'logicalor' => '0x2228', + 'intersection' => '0x2229', + 'union' => '0x222a', + 'integral' => '0x222b', + 'therefore' => '0x2234', + 'similar' => '0x223c', + 'congruent' => '0x2245', + 'approxequal' => '0x2248', + 'notequal' => '0x2260', + 'equivalence' => '0x2261', + 'lessequal' => '0x2264', + 'greaterequal' => '0x2265', + 'propersubset' => '0x2282', + 'propersuperset' => '0x2283', + 'notsubset' => '0x2284', + 'reflexsubset' => '0x2286', + 'reflexsuperset' => '0x2287', + 'circleplus' => '0x2295', + 'circlemultiply' => '0x2297', + 'perpendicular' => '0x22a5', + 'dotmath' => '0x22c5', + 'house' => '0x2302', + 'revlogicalnot' => '0x2310', + 'integraltp' => '0x2320', + 'integralbt' => '0x2321', + 'angleleft' => '0x2329', + 'angleright' => '0x232a', + 'SF100000' => '0x2500', + 'SF110000' => '0x2502', + 'SF010000' => '0x250c', + 'SF030000' => '0x2510', + 'SF020000' => '0x2514', + 'SF040000' => '0x2518', + 'SF080000' => '0x251c', + 'SF090000' => '0x2524', + 'SF060000' => '0x252c', + 'SF070000' => '0x2534', + 'SF050000' => '0x253c', + 'SF430000' => '0x2550', + 'SF240000' => '0x2551', + 'SF510000' => '0x2552', + 'SF520000' => '0x2553', + 'SF390000' => '0x2554', + 'SF220000' => '0x2555', + 'SF210000' => '0x2556', + 'SF250000' => '0x2557', + 'SF500000' => '0x2558', + 'SF490000' => '0x2559', + 'SF380000' => '0x255a', + 'SF280000' => '0x255b', + 'SF270000' => '0x255c', + 'SF260000' => '0x255d', + 'SF360000' => '0x255e', + 'SF370000' => '0x255f', + 'SF420000' => '0x2560', + 'SF190000' => '0x2561', + 'SF200000' => '0x2562', + 'SF230000' => '0x2563', + 'SF470000' => '0x2564', + 'SF480000' => '0x2565', + 'SF410000' => '0x2566', + 'SF450000' => '0x2567', + 'SF460000' => '0x2568', + 'SF400000' => '0x2569', + 'SF540000' => '0x256a', + 'SF530000' => '0x256b', + 'SF440000' => '0x256c', + 'upblock' => '0x2580', + 'dnblock' => '0x2584', + 'block' => '0x2588', + 'lfblock' => '0x258c', + 'rtblock' => '0x2590', + 'ltshade' => '0x2591', + 'shade' => '0x2592', + 'dkshade' => '0x2593', + 'filledbox' => '0x25a0', + 'H22073' => '0x25a1', + 'H18543' => '0x25aa', + 'H18551' => '0x25ab', + 'filledrect' => '0x25ac', + 'triagup' => '0x25b2', + 'triagrt' => '0x25ba', + 'triagdn' => '0x25bc', + 'triaglf' => '0x25c4', + 'lozenge' => '0x25ca', + 'circle' => '0x25cb', + 'H18533' => '0x25cf', + 'invbullet' => '0x25d8', + 'invcircle' => '0x25d9', + 'openbullet' => '0x25e6', + 'smileface' => '0x263a', + 'invsmileface' => '0x263b', + 'sun' => '0x263c', + 'female' => '0x2640', + 'male' => '0x2642', + 'spade' => '0x2660', + 'club' => '0x2663', + 'heart' => '0x2665', + 'diamond' => '0x2666', + 'musicalnote' => '0x266a', + 'musicalnotedbl' => '0x266b', + 'dotlessj' => '0xf6be', + 'LL' => '0xf6bf', + 'll' => '0xf6c0', + 'commaaccent' => '0xf6c3', + 'afii10063' => '0xf6c4', + 'afii10064' => '0xf6c5', + 'afii10192' => '0xf6c6', + 'afii10831' => '0xf6c7', + 'afii10832' => '0xf6c8', + 'Acute' => '0xf6c9', + 'Caron' => '0xf6ca', + 'Dieresis' => '0xf6cb', + 'DieresisAcute' => '0xf6cc', + 'DieresisGrave' => '0xf6cd', + 'Grave' => '0xf6ce', + 'Hungarumlaut' => '0xf6cf', + 'Macron' => '0xf6d0', + 'cyrBreve' => '0xf6d1', + 'cyrFlex' => '0xf6d2', + 'dblGrave' => '0xf6d3', + 'cyrbreve' => '0xf6d4', + 'cyrflex' => '0xf6d5', + 'dblgrave' => '0xf6d6', + 'dieresisacute' => '0xf6d7', + 'dieresisgrave' => '0xf6d8', + 'copyrightserif' => '0xf6d9', + 'registerserif' => '0xf6da', + 'trademarkserif' => '0xf6db', + 'onefitted' => '0xf6dc', + 'rupiah' => '0xf6dd', + 'threequartersemdash' => '0xf6de', + 'centinferior' => '0xf6df', + 'centsuperior' => '0xf6e0', + 'commainferior' => '0xf6e1', + 'commasuperior' => '0xf6e2', + 'dollarinferior' => '0xf6e3', + 'dollarsuperior' => '0xf6e4', + 'hypheninferior' => '0xf6e5', + 'hyphensuperior' => '0xf6e6', + 'periodinferior' => '0xf6e7', + 'periodsuperior' => '0xf6e8', + 'asuperior' => '0xf6e9', + 'bsuperior' => '0xf6ea', + 'dsuperior' => '0xf6eb', + 'esuperior' => '0xf6ec', + 'isuperior' => '0xf6ed', + 'lsuperior' => '0xf6ee', + 'msuperior' => '0xf6ef', + 'osuperior' => '0xf6f0', + 'rsuperior' => '0xf6f1', + 'ssuperior' => '0xf6f2', + 'tsuperior' => '0xf6f3', + 'Brevesmall' => '0xf6f4', + 'Caronsmall' => '0xf6f5', + 'Circumflexsmall' => '0xf6f6', + 'Dotaccentsmall' => '0xf6f7', + 'Hungarumlautsmall' => '0xf6f8', + 'Lslashsmall' => '0xf6f9', + 'OEsmall' => '0xf6fa', + 'Ogoneksmall' => '0xf6fb', + 'Ringsmall' => '0xf6fc', + 'Scaronsmall' => '0xf6fd', + 'Tildesmall' => '0xf6fe', + 'Zcaronsmall' => '0xf6ff', + 'exclamsmall' => '0xf721', + 'dollaroldstyle' => '0xf724', + 'ampersandsmall' => '0xf726', + 'zerooldstyle' => '0xf730', + 'oneoldstyle' => '0xf731', + 'twooldstyle' => '0xf732', + 'threeoldstyle' => '0xf733', + 'fouroldstyle' => '0xf734', + 'fiveoldstyle' => '0xf735', + 'sixoldstyle' => '0xf736', + 'sevenoldstyle' => '0xf737', + 'eightoldstyle' => '0xf738', + 'nineoldstyle' => '0xf739', + 'questionsmall' => '0xf73f', + 'Gravesmall' => '0xf760', + 'Asmall' => '0xf761', + 'Bsmall' => '0xf762', + 'Csmall' => '0xf763', + 'Dsmall' => '0xf764', + 'Esmall' => '0xf765', + 'Fsmall' => '0xf766', + 'Gsmall' => '0xf767', + 'Hsmall' => '0xf768', + 'Ismall' => '0xf769', + 'Jsmall' => '0xf76a', + 'Ksmall' => '0xf76b', + 'Lsmall' => '0xf76c', + 'Msmall' => '0xf76d', + 'Nsmall' => '0xf76e', + 'Osmall' => '0xf76f', + 'Psmall' => '0xf770', + 'Qsmall' => '0xf771', + 'Rsmall' => '0xf772', + 'Ssmall' => '0xf773', + 'Tsmall' => '0xf774', + 'Usmall' => '0xf775', + 'Vsmall' => '0xf776', + 'Wsmall' => '0xf777', + 'Xsmall' => '0xf778', + 'Ysmall' => '0xf779', + 'Zsmall' => '0xf77a', + 'exclamdownsmall' => '0xf7a1', + 'centoldstyle' => '0xf7a2', + 'Dieresissmall' => '0xf7a8', + 'Macronsmall' => '0xf7af', + 'Acutesmall' => '0xf7b4', + 'Cedillasmall' => '0xf7b8', + 'questiondownsmall' => '0xf7bf', + 'Agravesmall' => '0xf7e0', + 'Aacutesmall' => '0xf7e1', + 'Acircumflexsmall' => '0xf7e2', + 'Atildesmall' => '0xf7e3', + 'Adieresissmall' => '0xf7e4', + 'Aringsmall' => '0xf7e5', + 'AEsmall' => '0xf7e6', + 'Ccedillasmall' => '0xf7e7', + 'Egravesmall' => '0xf7e8', + 'Eacutesmall' => '0xf7e9', + 'Ecircumflexsmall' => '0xf7ea', + 'Edieresissmall' => '0xf7eb', + 'Igravesmall' => '0xf7ec', + 'Iacutesmall' => '0xf7ed', + 'Icircumflexsmall' => '0xf7ee', + 'Idieresissmall' => '0xf7ef', + 'Ethsmall' => '0xf7f0', + 'Ntildesmall' => '0xf7f1', + 'Ogravesmall' => '0xf7f2', + 'Oacutesmall' => '0xf7f3', + 'Ocircumflexsmall' => '0xf7f4', + 'Otildesmall' => '0xf7f5', + 'Odieresissmall' => '0xf7f6', + 'Oslashsmall' => '0xf7f8', + 'Ugravesmall' => '0xf7f9', + 'Uacutesmall' => '0xf7fa', + 'Ucircumflexsmall' => '0xf7fb', + 'Udieresissmall' => '0xf7fc', + 'Yacutesmall' => '0xf7fd', + 'Thornsmall' => '0xf7fe', + 'Ydieresissmall' => '0xf7ff', + 'radicalex' => '0xf8e5', + 'arrowvertex' => '0xf8e6', + 'arrowhorizex' => '0xf8e7', + 'registersans' => '0xf8e8', + 'copyrightsans' => '0xf8e9', + 'trademarksans' => '0xf8ea', + 'parenlefttp' => '0xf8eb', + 'parenleftex' => '0xf8ec', + 'parenleftbt' => '0xf8ed', + 'bracketlefttp' => '0xf8ee', + 'bracketleftex' => '0xf8ef', + 'bracketleftbt' => '0xf8f0', + 'bracelefttp' => '0xf8f1', + 'braceleftmid' => '0xf8f2', + 'braceleftbt' => '0xf8f3', + 'braceex' => '0xf8f4', + 'integralex' => '0xf8f5', + 'parenrighttp' => '0xf8f6', + 'parenrightex' => '0xf8f7', + 'parenrightbt' => '0xf8f8', + 'bracketrighttp' => '0xf8f9', + 'bracketrightex' => '0xf8fa', + 'bracketrightbt' => '0xf8fb', + 'bracerighttp' => '0xf8fc', + 'bracerightmid' => '0xf8fd', + 'bracerightbt' => '0xf8fe', + 'ff' => '0xfb00', + 'fi' => '0xfb01', + 'fl' => '0xfb02', + 'ffi' => '0xfb03', + 'ffl' => '0xfb04', + 'afii57705' => '0xfb1f', + 'afii57694' => '0xfb2a', + 'afii57695' => '0xfb2b', + 'afii57723' => '0xfb35', + 'afii57700' => '0xfb4b', + ]; + } + + public static function getCodePoint($glyph): ?int + { + $glyphsMap = static::getGlyphs(); + + if (isset($glyphsMap[$glyph])) { + return hexdec($glyphsMap[$glyph]); + } + + return null; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/StandardEncoding.php b/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/StandardEncoding.php new file mode 100644 index 0000000..01d0a1c --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/StandardEncoding.php @@ -0,0 +1,76 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +// Source : http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/StandardEncoding.pm + +namespace Smalot\PdfParser\Encoding; + +/** + * Class StandardEncoding + */ +class StandardEncoding extends AbstractEncoding +{ + public function getTranslations(): array + { + $encoding = + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + 'space exclam quotedbl numbersign dollar percent ampersand quoteright '. + 'parenleft parenright asterisk plus comma hyphen period slash zero '. + 'one two three four five six seven eight nine colon semicolon less '. + 'equal greater question at A B C D E F G H I J K L M N O P Q R S T U '. + 'V W X Y Z bracketleft backslash bracketright asciicircum underscore '. + 'quoteleft a b c d e f g h i j k l m n o p q r s t u v w x y z '. + 'braceleft bar braceright asciitilde .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef exclamdown cent '. + 'sterling fraction yen florin section currency quotesingle '. + 'quotedblleft guillemotleft guilsinglleft guilsinglright fi fl '. + '.notdef endash dagger daggerdbl periodcentered .notdef paragraph '. + 'bullet quotesinglbase quotedblbase quotedblright guillemotright '. + 'ellipsis perthousand .notdef questiondown .notdef grave acute '. + 'circumflex tilde macron breve dotaccent dieresis .notdef ring '. + 'cedilla .notdef hungarumlaut ogonek caron emdash .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef AE .notdef '. + 'ordfeminine .notdef .notdef .notdef .notdef Lslash Oslash OE '. + 'ordmasculine .notdef .notdef .notdef .notdef .notdef ae .notdef '. + '.notdef .notdef dotlessi .notdef .notdef lslash oslash oe germandbls '. + '.notdef .notdef .notdef .notdef'; + + return explode(' ', $encoding); + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/WinAnsiEncoding.php b/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/WinAnsiEncoding.php new file mode 100644 index 0000000..1938f55 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Encoding/WinAnsiEncoding.php @@ -0,0 +1,76 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +// Source : http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/WinANSIEncoding.pm + +namespace Smalot\PdfParser\Encoding; + +/** + * Class WinAnsiEncoding + */ +class WinAnsiEncoding extends AbstractEncoding +{ + public function getTranslations(): array + { + $encoding = + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + '.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '. + 'space exclam quotedbl numbersign dollar percent ampersand quotesingle '. + 'parenleft parenright asterisk plus comma hyphen period slash zero one '. + 'two three four five six seven eight nine colon semicolon less equal '. + 'greater question at A B C D E F G H I J K L M N O P Q R S T U V W X '. + 'Y Z bracketleft backslash bracketright asciicircum underscore '. + 'grave a b c d e f g h i j k l m n o p q r s t u v w x y z '. + 'braceleft bar braceright asciitilde bullet Euro bullet quotesinglbase '. + 'florin quotedblbase ellipsis dagger daggerdbl circumflex perthousand '. + 'Scaron guilsinglleft OE bullet Zcaron bullet bullet quoteleft quoteright '. + 'quotedblleft quotedblright bullet endash emdash tilde trademark scaron '. + 'guilsinglright oe bullet zcaron Ydieresis space exclamdown cent '. + 'sterling currency yen brokenbar section dieresis copyright '. + 'ordfeminine guillemotleft logicalnot hyphen registered macron degree '. + 'plusminus twosuperior threesuperior acute mu paragraph '. + 'periodcentered cedilla onesuperior ordmasculine guillemotright '. + 'onequarter onehalf threequarters questiondown Agrave Aacute '. + 'Acircumflex Atilde Adieresis Aring AE Ccedilla Egrave Eacute '. + 'Ecircumflex Edieresis Igrave Iacute Icircumflex Idieresis Eth Ntilde '. + 'Ograve Oacute Ocircumflex Otilde Odieresis multiply Oslash Ugrave '. + 'Uacute Ucircumflex Udieresis Yacute Thorn germandbls agrave aacute '. + 'acircumflex atilde adieresis aring ae ccedilla egrave eacute '. + 'ecircumflex edieresis igrave iacute icircumflex idieresis eth ntilde '. + 'ograve oacute ocircumflex otilde odieresis divide oslash ugrave '. + 'uacute ucircumflex udieresis yacute thorn ydieresis'; + + return explode(' ', $encoding); + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Exception/EncodingNotFoundException.php b/orcinus/pdfparser/src/Smalot/PdfParser/Exception/EncodingNotFoundException.php new file mode 100644 index 0000000..c43a66c --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Exception/EncodingNotFoundException.php @@ -0,0 +1,7 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser; + +use Smalot\PdfParser\Encoding\WinAnsiEncoding; +use Smalot\PdfParser\Exception\EncodingNotFoundException; + +/** + * Class Font + */ +class Font extends PDFObject +{ + public const MISSING = '?'; + + /** + * @var array + */ + protected $table = null; + + /** + * @var array + */ + protected $tableSizes = null; + + /** + * Caches results from uchr. + * + * @var array + */ + private static $uchrCache = []; + + /** + * In some PDF-files encoding could be referenced by object id but object itself does not contain + * `/Type /Encoding` in its dictionary. These objects wouldn't be initialized as Encoding in + * \Smalot\PdfParser\PDFObject::factory() during file parsing (they would be just PDFObject). + * + * Therefore, we create an instance of Encoding from them during decoding and cache this value in this property. + * + * @var Encoding + * + * @see https://github.com/smalot/pdfparser/pull/500 + */ + private $initializedEncodingByPdfObject; + + public function init() + { + // Load translate table. + $this->loadTranslateTable(); + } + + public function getName(): string + { + return $this->has('BaseFont') ? (string) $this->get('BaseFont') : '[Unknown]'; + } + + public function getType(): string + { + return (string) $this->header->get('Subtype'); + } + + public function getDetails(bool $deep = true): array + { + $details = []; + + $details['Name'] = $this->getName(); + $details['Type'] = $this->getType(); + $details['Encoding'] = ($this->has('Encoding') ? (string) $this->get('Encoding') : 'Ansi'); + + $details += parent::getDetails($deep); + + return $details; + } + + /** + * @return string|bool + */ + public function translateChar(string $char, bool $use_default = true) + { + $dec = hexdec(bin2hex($char)); + + if (\array_key_exists($dec, $this->table)) { + return $this->table[$dec]; + } + + // fallback for decoding single-byte ANSI characters that are not in the lookup table + $fallbackDecoded = $char; + if ( + \strlen($char) < 2 + && $this->has('Encoding') + && $this->get('Encoding') instanceof Encoding + ) { + try { + if (WinAnsiEncoding::class === $this->get('Encoding')->__toString()) { + $fallbackDecoded = self::uchr($dec); + } + } catch (EncodingNotFoundException $e) { + // Encoding->getEncodingClass() throws EncodingNotFoundException when BaseEncoding doesn't exists + // See table 5.11 on PDF 1.5 specs for more info + } + } + + return $use_default ? self::MISSING : $fallbackDecoded; + } + + /** + * Convert unicode character code to "utf-8" encoded string. + */ + public static function uchr(int $code): string + { + if (!isset(self::$uchrCache[$code])) { + // html_entity_decode() will not work with UTF-16 or UTF-32 char entities, + // therefore, we use mb_convert_encoding() instead + self::$uchrCache[$code] = mb_convert_encoding("&#{$code};", 'UTF-8', 'HTML-ENTITIES'); + } + + return self::$uchrCache[$code]; + } + + /** + * Init internal chars translation table by ToUnicode CMap. + */ + public function loadTranslateTable(): array + { + if (null !== $this->table) { + return $this->table; + } + + $this->table = []; + $this->tableSizes = [ + 'from' => 1, + 'to' => 1, + ]; + + if ($this->has('ToUnicode')) { + $content = $this->get('ToUnicode')->getContent(); + $matches = []; + + // Support for multiple spacerange sections + if (preg_match_all('/begincodespacerange(?P.*?)endcodespacerange/s', $content, $matches)) { + foreach ($matches['sections'] as $section) { + $regexp = '/<(?P[0-9A-F]+)> *<(?P[0-9A-F]+)>[ \r\n]+/is'; + + preg_match_all($regexp, $section, $matches); + + $this->tableSizes = [ + 'from' => max(1, \strlen(current($matches['from'])) / 2), + 'to' => max(1, \strlen(current($matches['to'])) / 2), + ]; + + break; + } + } + + // Support for multiple bfchar sections + if (preg_match_all('/beginbfchar(?P.*?)endbfchar/s', $content, $matches)) { + foreach ($matches['sections'] as $section) { + $regexp = '/<(?P[0-9A-F]+)> +<(?P[0-9A-F]+)>[ \r\n]+/is'; + + preg_match_all($regexp, $section, $matches); + + $this->tableSizes['from'] = max(1, \strlen(current($matches['from'])) / 2); + + foreach ($matches['from'] as $key => $from) { + $parts = preg_split( + '/([0-9A-F]{4})/i', + $matches['to'][$key], + 0, + \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE + ); + $text = ''; + foreach ($parts as $part) { + $text .= self::uchr(hexdec($part)); + } + $this->table[hexdec($from)] = $text; + } + } + } + + // Support for multiple bfrange sections + if (preg_match_all('/beginbfrange(?P.*?)endbfrange/s', $content, $matches)) { + foreach ($matches['sections'] as $section) { + // Support for : + $regexp = '/<(?P[0-9A-F]+)> *<(?P[0-9A-F]+)> *<(?P[0-9A-F]+)>[ \r\n]+/is'; + + preg_match_all($regexp, $section, $matches); + + foreach ($matches['from'] as $key => $from) { + $char_from = hexdec($from); + $char_to = hexdec($matches['to'][$key]); + $offset = hexdec($matches['offset'][$key]); + + for ($char = $char_from; $char <= $char_to; ++$char) { + $this->table[$char] = self::uchr($char - $char_from + $offset); + } + } + + // Support for : [ ... ] + // Some PDF file has 2-byte Unicode values on new lines > added \r\n + $regexp = '/<(?P[0-9A-F]+)> *<(?P[0-9A-F]+)> *\[(?P[\r\n<>0-9A-F ]+)\][ \r\n]+/is'; + + preg_match_all($regexp, $section, $matches); + + foreach ($matches['from'] as $key => $from) { + $char_from = hexdec($from); + $strings = []; + + preg_match_all('/<(?P[0-9A-F]+)> */is', $matches['strings'][$key], $strings); + + foreach ($strings['string'] as $position => $string) { + $parts = preg_split( + '/([0-9A-F]{4})/i', + $string, + 0, + \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE + ); + $text = ''; + foreach ($parts as $part) { + $text .= self::uchr(hexdec($part)); + } + $this->table[$char_from + $position] = $text; + } + } + } + } + } + + return $this->table; + } + + /** + * Set custom char translation table where: + * - key - integer character code; + * - value - "utf-8" encoded value; + * + * @return void + */ + public function setTable(array $table) + { + $this->table = $table; + } + + /** + * Calculate text width with data from header 'Widths'. If width of character is not found then character is added to missing array. + */ + public function calculateTextWidth(string $text, array &$missing = null): ?float + { + $index_map = array_flip($this->table); + $details = $this->getDetails(); + $widths = $details['Widths']; + + // Widths array is zero indexed but table is not. We must map them based on FirstChar and LastChar + $width_map = array_flip(range($details['FirstChar'], $details['LastChar'])); + + $width = null; + $missing = []; + $textLength = mb_strlen($text); + for ($i = 0; $i < $textLength; ++$i) { + $char = mb_substr($text, $i, 1); + if ( + !\array_key_exists($char, $index_map) + || !\array_key_exists($index_map[$char], $width_map) + || !\array_key_exists($width_map[$index_map[$char]], $widths) + ) { + $missing[] = $char; + continue; + } + $width_index = $width_map[$index_map[$char]]; + $width += $widths[$width_index]; + } + + return $width; + } + + /** + * Decode hexadecimal encoded string. If $add_braces is true result value would be wrapped by parentheses. + */ + public static function decodeHexadecimal(string $hexa, bool $add_braces = false): string + { + // Special shortcut for XML content. + if (false !== stripos($hexa, ')/si', $hexa, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE); + + foreach ($parts as $part) { + if (preg_match('/^<.*>$/s', $part) && false === stripos($part, ''); + if ($add_braces) { + $text .= '('; + } + + $part = pack('H*', $part); + $text .= ($add_braces ? preg_replace('/\\\/s', '\\\\\\', $part) : $part); + + if ($add_braces) { + $text .= ')'; + } + } else { + $text .= $part; + } + } + + return $text; + } + + /** + * Decode string with octal-decoded chunks. + */ + public static function decodeOctal(string $text): string + { + $parts = preg_split('/(\\\\[0-7]{3})/s', $text, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE); + $text = ''; + + foreach ($parts as $part) { + if (preg_match('/^\\\\[0-7]{3}$/', $part)) { + $text .= \chr(octdec(trim($part, '\\'))); + } else { + $text .= $part; + } + } + + return $text; + } + + /** + * Decode string with html entity encoded chars. + */ + public static function decodeEntities(string $text): string + { + $parts = preg_split('/(#\d{2})/s', $text, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE); + $text = ''; + + foreach ($parts as $part) { + if (preg_match('/^#\d{2}$/', $part)) { + $text .= \chr(hexdec(trim($part, '#'))); + } else { + $text .= $part; + } + } + + return $text; + } + + /** + * Check if given string is Unicode text (by BOM); + * If true - decode to "utf-8" encoded string. + * Otherwise - return text as is. + * + * @todo Rename in next major release to make the name correspond to reality (for ex. decodeIfUnicode()) + */ + public static function decodeUnicode(string $text): string + { + if (preg_match('/^\xFE\xFF/i', $text)) { + // Strip U+FEFF byte order marker. + $decode = substr($text, 2); + $text = ''; + $length = \strlen($decode); + + for ($i = 0; $i < $length; $i += 2) { + $text .= self::uchr(hexdec(bin2hex(substr($decode, $i, 2)))); + } + } + + return $text; + } + + /** + * @todo Deprecated, use $this->config->getFontSpaceLimit() instead. + */ + protected function getFontSpaceLimit(): int + { + return $this->config->getFontSpaceLimit(); + } + + /** + * Decode text by commands array. + */ + public function decodeText(array $commands): string + { + $word_position = 0; + $words = []; + $font_space = $this->getFontSpaceLimit(); + + foreach ($commands as $command) { + switch ($command[PDFObject::TYPE]) { + case 'n': + if ((float) trim($command[PDFObject::COMMAND]) < $font_space) { + $word_position = \count($words); + } + continue 2; + case '<': + // Decode hexadecimal. + $text = self::decodeHexadecimal('<'.$command[PDFObject::COMMAND].'>'); + break; + + default: + // Decode octal (if necessary). + $text = self::decodeOctal($command[PDFObject::COMMAND]); + } + + // replace escaped chars + $text = str_replace( + ['\\\\', '\(', '\)', '\n', '\r', '\t', '\f', '\ '], + ['\\', '(', ')', "\n", "\r", "\t", "\f", ' '], + $text + ); + + // add content to result string + if (isset($words[$word_position])) { + $words[$word_position] .= $text; + } else { + $words[$word_position] = $text; + } + } + + foreach ($words as &$word) { + $word = $this->decodeContent($word); + } + + return implode(' ', $words); + } + + /** + * Decode given $text to "utf-8" encoded string. + * + * @param bool $unicode This parameter is deprecated and might be removed in a future release + */ + public function decodeContent(string $text, ?bool &$unicode = null): string + { + if ($this->has('ToUnicode')) { + return $this->decodeContentByToUnicodeCMapOrDescendantFonts($text); + } + + if ($this->has('Encoding')) { + $result = $this->decodeContentByEncoding($text); + + if (null !== $result) { + return $result; + } + } + + return $this->decodeContentByAutodetectIfNecessary($text); + } + + /** + * First try to decode $text by ToUnicode CMap. + * If char translation not found in ToUnicode CMap tries: + * - If DescendantFonts exists tries to decode char by one of that fonts. + * - If have no success to decode by DescendantFonts interpret $text as a string with "Windows-1252" encoding. + * - If DescendantFonts does not exist just return "?" as decoded char. + * + * @todo Seems this is invalid algorithm that do not follow pdf-format specification. Must be rewritten. + */ + private function decodeContentByToUnicodeCMapOrDescendantFonts(string $text): string + { + $bytes = $this->tableSizes['from']; + + if ($bytes) { + $result = ''; + $length = \strlen($text); + + for ($i = 0; $i < $length; $i += $bytes) { + $char = substr($text, $i, $bytes); + + if (false !== ($decoded = $this->translateChar($char, false))) { + $char = $decoded; + } elseif ($this->has('DescendantFonts')) { + if ($this->get('DescendantFonts') instanceof PDFObject) { + $fonts = $this->get('DescendantFonts')->getHeader()->getElements(); + } else { + $fonts = $this->get('DescendantFonts')->getContent(); + } + $decoded = false; + + foreach ($fonts as $font) { + if ($font instanceof self) { + if (false !== ($decoded = $font->translateChar($char, false))) { + $decoded = mb_convert_encoding($decoded, 'UTF-8', 'Windows-1252'); + break; + } + } + } + + if (false !== $decoded) { + $char = $decoded; + } else { + $char = mb_convert_encoding($char, 'UTF-8', 'Windows-1252'); + } + } else { + $char = self::MISSING; + } + + $result .= $char; + } + + $text = $result; + } + + return $text; + } + + /** + * Decode content by any type of Encoding (dictionary's item) instance. + */ + private function decodeContentByEncoding(string $text): ?string + { + $encoding = $this->get('Encoding'); + + // When Encoding referenced by object id (/Encoding 520 0 R) but object itself does not contain `/Type /Encoding` in it's dictionary. + if ($encoding instanceof PDFObject) { + $encoding = $this->getInitializedEncodingByPdfObject($encoding); + } + + // When Encoding referenced by object id (/Encoding 520 0 R) but object itself contains `/Type /Encoding` in it's dictionary. + if ($encoding instanceof Encoding) { + return $this->decodeContentByEncodingEncoding($text, $encoding); + } + + // When Encoding is just string (/Encoding /WinAnsiEncoding) + if ($encoding instanceof Element) { // todo: ElementString class must by used? + return $this->decodeContentByEncodingElement($text, $encoding); + } + + // don't double-encode strings already in UTF-8 + if (!mb_check_encoding($text, 'UTF-8')) { + return mb_convert_encoding($text, 'UTF-8', 'Windows-1252'); + } + + return $text; + } + + /** + * Returns already created or create a new one if not created before Encoding instance by PDFObject instance. + */ + private function getInitializedEncodingByPdfObject(PDFObject $PDFObject): Encoding + { + if (!$this->initializedEncodingByPdfObject) { + $this->initializedEncodingByPdfObject = $this->createInitializedEncodingByPdfObject($PDFObject); + } + + return $this->initializedEncodingByPdfObject; + } + + /** + * Decode content when $encoding (given by $this->get('Encoding')) is instance of Encoding. + */ + private function decodeContentByEncodingEncoding(string $text, Encoding $encoding): string + { + $result = ''; + $length = \strlen($text); + + for ($i = 0; $i < $length; ++$i) { + $dec_av = hexdec(bin2hex($text[$i])); + $dec_ap = $encoding->translateChar($dec_av); + $result .= self::uchr($dec_ap ?? $dec_av); + } + + return $result; + } + + /** + * Decode content when $encoding (given by $this->get('Encoding')) is instance of Element. + */ + private function decodeContentByEncodingElement(string $text, Element $encoding): ?string + { + $pdfEncodingName = $encoding->getContent(); + + // mb_convert_encoding does not support MacRoman/macintosh, + // so we use iconv() here + $iconvEncodingName = $this->getIconvEncodingNameOrNullByPdfEncodingName($pdfEncodingName); + + return $iconvEncodingName ? iconv($iconvEncodingName, 'UTF-8', $text) : null; + } + + /** + * Convert PDF encoding name to iconv-known encoding name. + */ + private function getIconvEncodingNameOrNullByPdfEncodingName(string $pdfEncodingName): ?string + { + $pdfToIconvEncodingNameMap = [ + 'StandardEncoding' => 'ISO-8859-1', + 'MacRomanEncoding' => 'MACINTOSH', + 'WinAnsiEncoding' => 'CP1252', + ]; + + return \array_key_exists($pdfEncodingName, $pdfToIconvEncodingNameMap) + ? $pdfToIconvEncodingNameMap[$pdfEncodingName] + : null; + } + + /** + * If string seems like "utf-8" encoded string do nothing and just return given string as is. + * Otherwise, interpret string as "Window-1252" encoded string. + * + * @return string|false + */ + private function decodeContentByAutodetectIfNecessary(string $text) + { + if (mb_check_encoding($text, 'UTF-8')) { + return $text; + } + + return mb_convert_encoding($text, 'UTF-8', 'Windows-1252'); + // todo: Why exactly `Windows-1252` used? + } + + /** + * Create Encoding instance by PDFObject instance and init it. + */ + private function createInitializedEncodingByPdfObject(PDFObject $PDFObject): Encoding + { + $encoding = $this->createEncodingByPdfObject($PDFObject); + $encoding->init(); + + return $encoding; + } + + /** + * Create Encoding instance by PDFObject instance (without init). + */ + private function createEncodingByPdfObject(PDFObject $PDFObject): Encoding + { + $document = $PDFObject->getDocument(); + $header = $PDFObject->getHeader(); + $content = $PDFObject->getContent(); + $config = $PDFObject->getConfig(); + + return new Encoding($document, $header, $content, $config); + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontCIDFontType0.php b/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontCIDFontType0.php new file mode 100644 index 0000000..310c44c --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontCIDFontType0.php @@ -0,0 +1,42 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Font; + +use Smalot\PdfParser\Font; + +/** + * Class FontCIDFontType0 + */ +class FontCIDFontType0 extends Font +{ +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontCIDFontType2.php b/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontCIDFontType2.php new file mode 100644 index 0000000..077d6e7 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontCIDFontType2.php @@ -0,0 +1,42 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Font; + +use Smalot\PdfParser\Font; + +/** + * Class FontCIDFontType2 + */ +class FontCIDFontType2 extends Font +{ +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontTrueType.php b/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontTrueType.php new file mode 100644 index 0000000..8a55c00 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontTrueType.php @@ -0,0 +1,42 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Font; + +use Smalot\PdfParser\Font; + +/** + * Class FontTrueType + */ +class FontTrueType extends Font +{ +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontType0.php b/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontType0.php new file mode 100644 index 0000000..4e5cc6d --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontType0.php @@ -0,0 +1,42 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Font; + +use Smalot\PdfParser\Font; + +/** + * Class FontType0 + */ +class FontType0 extends Font +{ +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontType1.php b/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontType1.php new file mode 100644 index 0000000..ee93e69 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontType1.php @@ -0,0 +1,42 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Font; + +use Smalot\PdfParser\Font; + +/** + * Class FontType1 + */ +class FontType1 extends Font +{ +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontType3.php b/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontType3.php new file mode 100644 index 0000000..08f8da0 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Font/FontType3.php @@ -0,0 +1,42 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\Font; + +use Smalot\PdfParser\Font; + +/** + * Class FontType3 + */ +class FontType3 extends Font +{ +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Header.php b/orcinus/pdfparser/src/Smalot/PdfParser/Header.php new file mode 100644 index 0000000..66a7010 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Header.php @@ -0,0 +1,194 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser; + +use Smalot\PdfParser\Element\ElementArray; +use Smalot\PdfParser\Element\ElementMissing; +use Smalot\PdfParser\Element\ElementStruct; +use Smalot\PdfParser\Element\ElementXRef; + +/** + * Class Header + */ +class Header +{ + /** + * @var Document + */ + protected $document = null; + + /** + * @var Element[] + */ + protected $elements = null; + + /** + * @param Element[] $elements list of elements + * @param Document $document document + */ + public function __construct(array $elements = [], ?Document $document = null) + { + $this->elements = $elements; + $this->document = $document; + } + + public function init() + { + foreach ($this->elements as $element) { + if ($element instanceof Element) { + $element->init(); + } + } + } + + /** + * Returns all elements. + */ + public function getElements() + { + foreach ($this->elements as $name => $element) { + $this->resolveXRef($name); + } + + return $this->elements; + } + + /** + * Used only for debug. + */ + public function getElementTypes(): array + { + $types = []; + + foreach ($this->elements as $key => $element) { + $types[$key] = \get_class($element); + } + + return $types; + } + + public function getDetails(bool $deep = true): array + { + $values = []; + $elements = $this->getElements(); + + foreach ($elements as $key => $element) { + if ($element instanceof self && $deep) { + $values[$key] = $element->getDetails($deep); + } elseif ($element instanceof PDFObject && $deep) { + $values[$key] = $element->getDetails(false); + } elseif ($element instanceof ElementArray) { + if ($deep) { + $values[$key] = $element->getDetails(); + } + } elseif ($element instanceof Element) { + $values[$key] = (string) $element; + } + } + + return $values; + } + + /** + * Indicate if an element name is available in header. + * + * @param string $name the name of the element + */ + public function has(string $name): bool + { + return \array_key_exists($name, $this->elements); + } + + /** + * @return Element|PDFObject + */ + public function get(string $name) + { + if (\array_key_exists($name, $this->elements) && $element = $this->resolveXRef($name)) { + return $element; + } + + return new ElementMissing(); + } + + /** + * Resolve XRef to object. + * + * @return Element|PDFObject + * + * @throws \Exception + */ + protected function resolveXRef(string $name) + { + if (($obj = $this->elements[$name]) instanceof ElementXRef && null !== $this->document) { + /** @var ElementXRef $obj */ + $object = $this->document->getObjectById($obj->getId()); + + if (null === $object) { + return new ElementMissing(); + } + + // Update elements list for future calls. + $this->elements[$name] = $object; + } + + return $this->elements[$name]; + } + + /** + * @param string $content The content to parse + * @param Document $document The document + * @param int $position The new position of the cursor after parsing + */ + public static function parse(string $content, Document $document, int &$position = 0): self + { + /* @var Header $header */ + if ('<<' == substr(trim($content), 0, 2)) { + $header = ElementStruct::parse($content, $document, $position); + } else { + $elements = ElementArray::parse($content, $document, $position); + $header = new self([], $document); + + if ($elements) { + $header = new self($elements->getRawContent(), null); + } + } + + if ($header) { + return $header; + } + + // Build an empty header. + return new self([], $document); + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/PDFObject.php b/orcinus/pdfparser/src/Smalot/PdfParser/PDFObject.php new file mode 100644 index 0000000..57c0f89 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/PDFObject.php @@ -0,0 +1,779 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser; + +use Smalot\PdfParser\XObject\Form; +use Smalot\PdfParser\XObject\Image; + +/** + * Class PDFObject + */ +class PDFObject +{ + public const TYPE = 't'; + + public const OPERATOR = 'o'; + + public const COMMAND = 'c'; + + /** + * The recursion stack. + * + * @var array + */ + public static $recursionStack = []; + + /** + * @var Document + */ + protected $document = null; + + /** + * @var Header + */ + protected $header = null; + + /** + * @var string + */ + protected $content = null; + + /** + * @var Config + */ + protected $config; + + public function __construct( + Document $document, + ?Header $header = null, + ?string $content = null, + ?Config $config = null + ) { + $this->document = $document; + $this->header = $header ?? new Header(); + $this->content = $content; + $this->config = $config; + } + + public function init() + { + } + + public function getDocument(): Document + { + return $this->document; + } + + public function getHeader(): ?Header + { + return $this->header; + } + + public function getConfig(): ?Config + { + return $this->config; + } + + /** + * @return Element|PDFObject|Header + */ + public function get(string $name) + { + return $this->header->get($name); + } + + public function has(string $name): bool + { + return $this->header->has($name); + } + + public function getDetails(bool $deep = true): array + { + return $this->header->getDetails($deep); + } + + public function getContent(): ?string + { + return $this->content; + } + + public function cleanContent(string $content, string $char = 'X') + { + $char = $char[0]; + $content = str_replace(['\\\\', '\\)', '\\('], $char.$char, $content); + + // Remove image bloc with binary content + preg_match_all('/\s(BI\s.*?(\sID\s).*?(\sEI))\s/s', $content, $matches, \PREG_OFFSET_CAPTURE); + foreach ($matches[0] as $part) { + $content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0])); + } + + // Clean content in square brackets [.....] + preg_match_all('/\[((\(.*?\)|[0-9\.\-\s]*)*)\]/s', $content, $matches, \PREG_OFFSET_CAPTURE); + foreach ($matches[1] as $part) { + $content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0])); + } + + // Clean content in round brackets (.....) + preg_match_all('/\((.*?)\)/s', $content, $matches, \PREG_OFFSET_CAPTURE); + foreach ($matches[1] as $part) { + $content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0])); + } + + // Clean structure + if ($parts = preg_split('/(<|>)/s', $content, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE)) { + $content = ''; + $level = 0; + foreach ($parts as $part) { + if ('<' == $part) { + ++$level; + } + + $content .= (0 == $level ? $part : str_repeat($char, \strlen($part))); + + if ('>' == $part) { + --$level; + } + } + } + + // Clean BDC and EMC markup + preg_match_all( + '/(\/[A-Za-z0-9\_]*\s*'.preg_quote($char).'*BDC)/s', + $content, + $matches, + \PREG_OFFSET_CAPTURE + ); + foreach ($matches[1] as $part) { + $content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0])); + } + + preg_match_all('/\s(EMC)\s/s', $content, $matches, \PREG_OFFSET_CAPTURE); + foreach ($matches[1] as $part) { + $content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0])); + } + + return $content; + } + + public function getSectionsText(?string $content): array + { + $sections = []; + $content = ' '.$content.' '; + $textCleaned = $this->cleanContent($content, '_'); + + // Extract text blocks. + if (preg_match_all('/(\sQ)?\s+BT[\s|\(|\[]+(.*?)\s*ET(\sq)?/s', $textCleaned, $matches, \PREG_OFFSET_CAPTURE)) { + foreach ($matches[2] as $pos => $part) { + $text = $part[0]; + if ('' === $text) { + continue; + } + $offset = $part[1]; + $section = substr($content, $offset, \strlen($text)); + + // Removes BDC and EMC markup. + $section = preg_replace('/(\/[A-Za-z0-9]+\s*<<.*?)(>>\s*BDC)(.*?)(EMC\s+)/s', '${3}', $section.' '); + + // Add Q and q flags if detected around BT/ET. + // @see: https://github.com/smalot/pdfparser/issues/387 + $section = trim((!empty($matches[1][$pos][0]) ? "Q\n" : '').$section).(!empty($matches[3][$pos][0]) ? "\nq" : ''); + + $sections[] = $section; + } + } + + // Extract 'do' commands. + if (preg_match_all('/(\/[A-Za-z0-9\.\-_]+\s+Do)\s/s', $textCleaned, $matches, \PREG_OFFSET_CAPTURE)) { + foreach ($matches[1] as $part) { + $text = $part[0]; + $offset = $part[1]; + $section = substr($content, $offset, \strlen($text)); + + $sections[] = $section; + } + } + + return $sections; + } + + private function getDefaultFont(Page $page = null): Font + { + $fonts = []; + if (null !== $page) { + $fonts = $page->getFonts(); + } + + $firstFont = $this->document->getFirstFont(); + if (null !== $firstFont) { + $fonts[] = $firstFont; + } + + if (\count($fonts) > 0) { + return reset($fonts); + } + + return new Font($this->document, null, null, $this->config); + } + + /** + * @throws \Exception + */ + public function getText(?Page $page = null): string + { + $result = ''; + $sections = $this->getSectionsText($this->content); + $current_font = $this->getDefaultFont($page); + $clipped_font = $current_font; + + $current_position_td = ['x' => false, 'y' => false]; + $current_position_tm = ['x' => false, 'y' => false]; + + self::$recursionStack[] = $this->getUniqueId(); + + foreach ($sections as $section) { + $commands = $this->getCommandsText($section); + $reverse_text = false; + $text = ''; + + foreach ($commands as $command) { + switch ($command[self::OPERATOR]) { + case 'BMC': + if ('ReversedChars' == $command[self::COMMAND]) { + $reverse_text = true; + } + break; + + // set character spacing + case 'Tc': + break; + + // move text current point + case 'Td': + $args = preg_split('/\s/s', $command[self::COMMAND]); + $y = array_pop($args); + $x = array_pop($args); + if (((float) $x <= 0) || + (false !== $current_position_td['y'] && (float) $y < (float) $current_position_td['y']) + ) { + // vertical offset + $text .= "\n"; + } elseif (false !== $current_position_td['x'] && (float) $x > (float) + $current_position_td['x'] + ) { + $text .= $this->config->getHorizontalOffset(); + } + $current_position_td = ['x' => $x, 'y' => $y]; + break; + + // move text current point and set leading + case 'TD': + $args = preg_split('/\s/s', $command[self::COMMAND]); + $y = array_pop($args); + $x = array_pop($args); + if ((float) $y < 0) { + $text .= "\n"; + } elseif ((float) $x <= 0) { + $text .= ' '; + } + break; + + case 'Tf': + list($id) = preg_split('/\s/s', $command[self::COMMAND]); + $id = trim($id, '/'); + if (null !== $page) { + $new_font = $page->getFont($id); + // If an invalid font ID is given, do not update the font. + // This should theoretically never happen, as the PDF spec states for the Tf operator: + // "The specified font value shall match a resource name in the Font entry of the default resource dictionary" + // (https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf, page 435) + // But we want to make sure that malformed PDFs do not simply crash. + if (null !== $new_font) { + $current_font = $new_font; + } + } + break; + + case 'Q': + // Use clip: restore font. + $current_font = $clipped_font; + break; + + case 'q': + // Use clip: save font. + $clipped_font = $current_font; + break; + + case "'": + case 'Tj': + $command[self::COMMAND] = [$command]; + // no break + case 'TJ': + $sub_text = $current_font->decodeText($command[self::COMMAND]); + $text .= $sub_text; + break; + + // set leading + case 'TL': + $text .= ' '; + break; + + case 'Tm': + $args = preg_split('/\s/s', $command[self::COMMAND]); + $y = array_pop($args); + $x = array_pop($args); + if (false !== $current_position_tm['x']) { + $delta = abs((float) $x - (float) $current_position_tm['x']); + if ($delta > 10) { + $text .= "\t"; + } + } + if (false !== $current_position_tm['y']) { + $delta = abs((float) $y - (float) $current_position_tm['y']); + if ($delta > 10) { + $text .= "\n"; + } + } + $current_position_tm = ['x' => $x, 'y' => $y]; + break; + + // set super/subscripting text rise + case 'Ts': + break; + + // set word spacing + case 'Tw': + break; + + // set horizontal scaling + case 'Tz': + $text .= "\n"; + break; + + // move to start of next line + case 'T*': + $text .= "\n"; + break; + + case 'Da': + break; + + case 'Do': + if (null !== $page) { + $args = preg_split('/\s/s', $command[self::COMMAND]); + $id = trim(array_pop($args), '/ '); + $xobject = $page->getXObject($id); + + // @todo $xobject could be a ElementXRef object, which would then throw an error + if (\is_object($xobject) && $xobject instanceof self && !\in_array($xobject->getUniqueId(), self::$recursionStack)) { + // Not a circular reference. + $text .= $xobject->getText($page); + } + } + break; + + case 'rg': + case 'RG': + break; + + case 're': + break; + + case 'co': + break; + + case 'cs': + break; + + case 'gs': + break; + + case 'en': + break; + + case 'sc': + case 'SC': + break; + + case 'g': + case 'G': + break; + + case 'V': + break; + + case 'vo': + case 'Vo': + break; + + default: + } + } + + // Fix Hebrew and other reverse text oriented languages. + // @see: https://github.com/smalot/pdfparser/issues/398 + if ($reverse_text) { + $chars = mb_str_split($text, 1, mb_internal_encoding()); + $text = implode('', array_reverse($chars)); + } + + $result .= $text; + } + + return $result.' '; + } + + /** + * @throws \Exception + */ + public function getTextArray(?Page $page = null): array + { + $text = []; + $sections = $this->getSectionsText($this->content); + $current_font = new Font($this->document, null, null, $this->config); + + foreach ($sections as $section) { + $commands = $this->getCommandsText($section); + + foreach ($commands as $command) { + switch ($command[self::OPERATOR]) { + // set character spacing + case 'Tc': + break; + + // move text current point + case 'Td': + break; + + // move text current point and set leading + case 'TD': + break; + + case 'Tf': + if (null !== $page) { + list($id) = preg_split('/\s/s', $command[self::COMMAND]); + $id = trim($id, '/'); + $current_font = $page->getFont($id); + } + break; + + case "'": + case 'Tj': + $command[self::COMMAND] = [$command]; + // no break + case 'TJ': + $sub_text = $current_font->decodeText($command[self::COMMAND]); + $text[] = $sub_text; + break; + + // set leading + case 'TL': + break; + + case 'Tm': + break; + + // set super/subscripting text rise + case 'Ts': + break; + + // set word spacing + case 'Tw': + break; + + // set horizontal scaling + case 'Tz': + // $text .= "\n"; + break; + + // move to start of next line + case 'T*': + // $text .= "\n"; + break; + + case 'Da': + break; + + case 'Do': + if (null !== $page) { + $args = preg_split('/\s/s', $command[self::COMMAND]); + $id = trim(array_pop($args), '/ '); + if ($xobject = $page->getXObject($id)) { + $text[] = $xobject->getText($page); + } + } + break; + + case 'rg': + case 'RG': + break; + + case 're': + break; + + case 'co': + break; + + case 'cs': + break; + + case 'gs': + break; + + case 'en': + break; + + case 'sc': + case 'SC': + break; + + case 'g': + case 'G': + break; + + case 'V': + break; + + case 'vo': + case 'Vo': + break; + + default: + } + } + } + + return $text; + } + + public function getCommandsText(string $text_part, int &$offset = 0): array + { + $commands = $matches = []; + + while ($offset < \strlen($text_part)) { + $offset += strspn($text_part, "\x00\x09\x0a\x0c\x0d\x20", $offset); + $char = $text_part[$offset]; + + $operator = ''; + $type = ''; + $command = false; + + switch ($char) { + case '/': + $type = $char; + if (preg_match( + '/^\/([A-Z0-9\._,\+]+\s+[0-9.\-]+)\s+([A-Z]+)\s*/si', + substr($text_part, $offset), + $matches + ) + ) { + $operator = $matches[2]; + $command = $matches[1]; + $offset += \strlen($matches[0]); + } elseif (preg_match( + '/^\/([A-Z0-9\._,\+]+)\s+([A-Z]+)\s*/si', + substr($text_part, $offset), + $matches + ) + ) { + $operator = $matches[2]; + $command = $matches[1]; + $offset += \strlen($matches[0]); + } + break; + + case '[': + case ']': + // array object + $type = $char; + if ('[' == $char) { + ++$offset; + // get elements + $command = $this->getCommandsText($text_part, $offset); + + if (preg_match('/^\s*[A-Z]{1,2}\s*/si', substr($text_part, $offset), $matches)) { + $operator = trim($matches[0]); + $offset += \strlen($matches[0]); + } + } else { + ++$offset; + break; + } + break; + + case '<': + case '>': + // array object + $type = $char; + ++$offset; + if ('<' == $char) { + $strpos = strpos($text_part, '>', $offset); + $command = substr($text_part, $offset, $strpos - $offset); + $offset = $strpos + 1; + } + + if (preg_match('/^\s*[A-Z]{1,2}\s*/si', substr($text_part, $offset), $matches)) { + $operator = trim($matches[0]); + $offset += \strlen($matches[0]); + } + break; + + case '(': + case ')': + ++$offset; + $type = $char; + $strpos = $offset; + if ('(' == $char) { + $open_bracket = 1; + while ($open_bracket > 0) { + if (!isset($text_part[$strpos])) { + break; + } + $ch = $text_part[$strpos]; + switch ($ch) { + case '\\': + // REVERSE SOLIDUS (5Ch) (Backslash) + // skip next character + ++$strpos; + break; + + case '(': + // LEFT PARENHESIS (28h) + ++$open_bracket; + break; + + case ')': + // RIGHT PARENTHESIS (29h) + --$open_bracket; + break; + } + ++$strpos; + } + $command = substr($text_part, $offset, $strpos - $offset - 1); + $offset = $strpos; + + if (preg_match('/^\s*([A-Z\']{1,2})\s*/si', substr($text_part, $offset), $matches)) { + $operator = $matches[1]; + $offset += \strlen($matches[0]); + } + } + break; + + default: + if ('ET' == substr($text_part, $offset, 2)) { + break; + } elseif (preg_match( + '/^\s*(?P([0-9\.\-]+\s*?)+)\s+(?P[A-Z]{1,3})\s*/si', + substr($text_part, $offset), + $matches + ) + ) { + $operator = trim($matches['id']); + $command = trim($matches['data']); + $offset += \strlen($matches[0]); + } elseif (preg_match('/^\s*([0-9\.\-]+\s*?)+\s*/si', substr($text_part, $offset), $matches)) { + $type = 'n'; + $command = trim($matches[0]); + $offset += \strlen($matches[0]); + } elseif (preg_match('/^\s*([A-Z\*]+)\s*/si', substr($text_part, $offset), $matches)) { + $type = ''; + $operator = $matches[1]; + $command = ''; + $offset += \strlen($matches[0]); + } + } + + if (false !== $command) { + $commands[] = [ + self::TYPE => $type, + self::OPERATOR => $operator, + self::COMMAND => $command, + ]; + } else { + break; + } + } + + return $commands; + } + + public static function factory( + Document $document, + Header $header, + ?string $content, + ?Config $config = null + ): self { + switch ($header->get('Type')->getContent()) { + case 'XObject': + switch ($header->get('Subtype')->getContent()) { + case 'Image': + return new Image($document, $header, $config->getRetainImageContent() ? $content : null, $config); + + case 'Form': + return new Form($document, $header, $content, $config); + } + + return new self($document, $header, $content, $config); + + case 'Pages': + return new Pages($document, $header, $content, $config); + + case 'Page': + return new Page($document, $header, $content, $config); + + case 'Encoding': + return new Encoding($document, $header, $content, $config); + + case 'Font': + $subtype = $header->get('Subtype')->getContent(); + $classname = '\Smalot\PdfParser\Font\Font'.$subtype; + + if (class_exists($classname)) { + return new $classname($document, $header, $content, $config); + } + + return new Font($document, $header, $content, $config); + + default: + return new self($document, $header, $content, $config); + } + } + + /** + * Returns unique id identifying the object. + */ + protected function getUniqueId(): string + { + return spl_object_hash($this); + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Page.php b/orcinus/pdfparser/src/Smalot/PdfParser/Page.php new file mode 100644 index 0000000..55121a9 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Page.php @@ -0,0 +1,953 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser; + +use Smalot\PdfParser\Element\ElementArray; +use Smalot\PdfParser\Element\ElementMissing; +use Smalot\PdfParser\Element\ElementNull; +use Smalot\PdfParser\Element\ElementXRef; + +class Page extends PDFObject +{ + /** + * @var Font[] + */ + protected $fonts = null; + + /** + * @var PDFObject[] + */ + protected $xobjects = null; + + /** + * @var array + */ + protected $dataTm = null; + + /** + * @return Font[] + */ + public function getFonts() + { + if (null !== $this->fonts) { + return $this->fonts; + } + + $resources = $this->get('Resources'); + + if (method_exists($resources, 'has') && $resources->has('Font')) { + if ($resources->get('Font') instanceof ElementMissing) { + return []; + } + + if ($resources->get('Font') instanceof Header) { + $fonts = $resources->get('Font')->getElements(); + } else { + $fonts = $resources->get('Font')->getHeader()->getElements(); + } + + $table = []; + + foreach ($fonts as $id => $font) { + if ($font instanceof Font) { + $table[$id] = $font; + + // Store too on cleaned id value (only numeric) + $id = preg_replace('/[^0-9\.\-_]/', '', $id); + if ('' != $id) { + $table[$id] = $font; + } + } + } + + return $this->fonts = $table; + } + + return []; + } + + public function getFont(string $id): ?Font + { + $fonts = $this->getFonts(); + + if (isset($fonts[$id])) { + return $fonts[$id]; + } + + // According to the PDF specs (https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf, page 238) + // "The font resource name presented to the Tf operator is arbitrary, as are the names for all kinds of resources" + // Instead, we search for the unfiltered name first and then do this cleaning as a fallback, so all tests still pass. + + if (isset($fonts[$id])) { + return $fonts[$id]; + } else { + $id = preg_replace('/[^0-9\.\-_]/', '', $id); + if (isset($fonts[$id])) { + return $fonts[$id]; + } + } + + return null; + } + + /** + * Support for XObject + * + * @return PDFObject[] + */ + public function getXObjects() + { + if (null !== $this->xobjects) { + return $this->xobjects; + } + + $resources = $this->get('Resources'); + + if (method_exists($resources, 'has') && $resources->has('XObject')) { + if ($resources->get('XObject') instanceof Header) { + $xobjects = $resources->get('XObject')->getElements(); + } else { + $xobjects = $resources->get('XObject')->getHeader()->getElements(); + } + + $table = []; + + foreach ($xobjects as $id => $xobject) { + $table[$id] = $xobject; + + // Store too on cleaned id value (only numeric) + $id = preg_replace('/[^0-9\.\-_]/', '', $id); + if ('' != $id) { + $table[$id] = $xobject; + } + } + + return $this->xobjects = $table; + } + + return []; + } + + public function getXObject(string $id): ?PDFObject + { + $xobjects = $this->getXObjects(); + + if (isset($xobjects[$id])) { + return $xobjects[$id]; + } + + return null; + /*$id = preg_replace('/[^0-9\.\-_]/', '', $id); + + if (isset($xobjects[$id])) { + return $xobjects[$id]; + } else { + return null; + }*/ + } + + public function getText(self $page = null): string + { + if ($contents = $this->get('Contents')) { + if ($contents instanceof ElementMissing) { + return ''; + } elseif ($contents instanceof ElementNull) { + return ''; + } elseif ($contents instanceof PDFObject) { + $elements = $contents->getHeader()->getElements(); + + if (is_numeric(key($elements))) { + $new_content = ''; + + foreach ($elements as $element) { + if ($element instanceof ElementXRef) { + $new_content .= $element->getObject()->getContent(); + } else { + $new_content .= $element->getContent(); + } + } + + $header = new Header([], $this->document); + $contents = new PDFObject($this->document, $header, $new_content, $this->config); + } + } elseif ($contents instanceof ElementArray) { + // Create a virtual global content. + $new_content = ''; + + foreach ($contents->getContent() as $content) { + $new_content .= $content->getContent()."\n"; + } + + $header = new Header([], $this->document); + $contents = new PDFObject($this->document, $header, $new_content, $this->config); + } + + /* + * Elements referencing each other on the same page can cause endless loops during text parsing. + * To combat this we keep a recursionStack containing already parsed elements on the page. + * The stack is only emptied here after getting text from a page. + */ + $contentsText = $contents->getText($this); + PDFObject::$recursionStack = []; + + return $contentsText; + } + + return ''; + } + + /** + * Return true if the current page is a (setasign\Fpdi\Fpdi) FPDI/FPDF document + * + * The metadata 'Producer' should have the value of "FPDF" . FPDF_VERSION if the + * pdf file was generated by FPDF/Fpfi. + * + * @return bool true is the current page is a FPDI/FPDF document + */ + public function isFpdf(): bool + { + if (\array_key_exists('Producer', $this->document->getDetails()) && + \is_string($this->document->getDetails()['Producer']) && + 0 === strncmp($this->document->getDetails()['Producer'], 'FPDF', 4)) { + return true; + } + + return false; + } + + /** + * Return the page number of the PDF document of the page object + * + * @return int the page number + */ + public function getPageNumber(): int + { + $pages = $this->document->getPages(); + $numOfPages = \count($pages); + for ($pageNum = 0; $pageNum < $numOfPages; ++$pageNum) { + if ($pages[$pageNum] === $this) { + break; + } + } + + return $pageNum; + } + + /** + * Return the Object of the page if the document is a FPDF/FPDI document + * + * If the document was generated by FPDF/FPDI it returns the + * PDFObject of the given page + * + * @return PDFObject The PDFObject for the page + */ + public function getPDFObjectForFpdf(): PDFObject + { + $pageNum = $this->getPageNumber(); + $xObjects = $this->getXObjects(); + + return $xObjects[$pageNum]; + } + + /** + * Return a new PDFObject of the document created with FPDF/FPDI + * + * For a document generated by FPDF/FPDI, it generates a + * new PDFObject for that document + * + * @return PDFObject The PDFObject + */ + public function createPDFObjectForFpdf(): PDFObject + { + $pdfObject = $this->getPDFObjectForFpdf(); + $new_content = $pdfObject->getContent(); + $header = $pdfObject->getHeader(); + $config = $pdfObject->config; + + return new PDFObject($pdfObject->document, $header, $new_content, $config); + } + + /** + * Return page if document is a FPDF/FPDI document + * + * @return Page The page + */ + public function createPageForFpdf(): self + { + $pdfObject = $this->getPDFObjectForFpdf(); + $new_content = $pdfObject->getContent(); + $header = $pdfObject->getHeader(); + $config = $pdfObject->config; + + return new self($pdfObject->document, $header, $new_content, $config); + } + + public function getTextArray(self $page = null): array + { + if ($this->isFpdf()) { + $pdfObject = $this->getPDFObjectForFpdf(); + $newPdfObject = $this->createPDFObjectForFpdf(); + + return $newPdfObject->getTextArray($pdfObject); + } else { + if ($contents = $this->get('Contents')) { + if ($contents instanceof ElementMissing) { + return []; + } elseif ($contents instanceof ElementNull) { + return []; + } elseif ($contents instanceof PDFObject) { + $elements = $contents->getHeader()->getElements(); + + if (is_numeric(key($elements))) { + $new_content = ''; + + /** @var PDFObject $element */ + foreach ($elements as $element) { + if ($element instanceof ElementXRef) { + $new_content .= $element->getObject()->getContent(); + } else { + $new_content .= $element->getContent(); + } + } + + $header = new Header([], $this->document); + $contents = new PDFObject($this->document, $header, $new_content, $this->config); + } else { + try { + $contents->getTextArray($this); + } catch (\Throwable $e) { + return $contents->getTextArray(); + } + } + } elseif ($contents instanceof ElementArray) { + // Create a virtual global content. + $new_content = ''; + + /** @var PDFObject $content */ + foreach ($contents->getContent() as $content) { + $new_content .= $content->getContent()."\n"; + } + + $header = new Header([], $this->document); + $contents = new PDFObject($this->document, $header, $new_content, $this->config); + } + + return $contents->getTextArray($this); + } + + return []; + } + } + + /** + * Gets all the text data with its internal representation of the page. + * + * Returns an array with the data and the internal representation + */ + public function extractRawData(): array + { + /* + * Now you can get the complete content of the object with the text on it + */ + $extractedData = []; + $content = $this->get('Contents'); + $values = $content->getContent(); + if (isset($values) && \is_array($values)) { + $text = ''; + foreach ($values as $section) { + $text .= $section->getContent(); + } + $sectionsText = $this->getSectionsText($text); + foreach ($sectionsText as $sectionText) { + $commandsText = $this->getCommandsText($sectionText); + foreach ($commandsText as $command) { + $extractedData[] = $command; + } + } + } else { + if ($this->isFpdf()) { + $content = $this->getPDFObjectForFpdf(); + } + $sectionsText = $content->getSectionsText($content->getContent()); + foreach ($sectionsText as $sectionText) { + $extractedData[] = ['t' => '', 'o' => 'BT', 'c' => '']; + + $commandsText = $content->getCommandsText($sectionText); + foreach ($commandsText as $command) { + $extractedData[] = $command; + } + } + } + + return $extractedData; + } + + /** + * Gets all the decoded text data with it internal representation from a page. + * + * @param array $extractedRawData the extracted data return by extractRawData or + * null if extractRawData should be called + * + * @return array An array with the data and the internal representation + */ + public function extractDecodedRawData(array $extractedRawData = null): array + { + if (!isset($extractedRawData) || !$extractedRawData) { + $extractedRawData = $this->extractRawData(); + } + $currentFont = null; /** @var Font $currentFont */ + $clippedFont = null; + $fpdfPage = null; + if ($this->isFpdf()) { + $fpdfPage = $this->createPageForFpdf(); + } + foreach ($extractedRawData as &$command) { + if ('Tj' == $command['o'] || 'TJ' == $command['o']) { + $data = $command['c']; + if (!\is_array($data)) { + $tmpText = ''; + if (isset($currentFont)) { + $tmpText = $currentFont->decodeOctal($data); + // $tmpText = $currentFont->decodeHexadecimal($tmpText, false); + } + $tmpText = str_replace( + ['\\\\', '\(', '\)', '\n', '\r', '\t', '\ '], + ['\\', '(', ')', "\n", "\r", "\t", ' '], + $tmpText + ); + $tmpText = mb_convert_encoding($tmpText, 'UTF-8', 'ISO-8859-1'); + if (isset($currentFont)) { + $tmpText = $currentFont->decodeContent($tmpText); + } + $command['c'] = $tmpText; + continue; + } + $numText = \count($data); + for ($i = 0; $i < $numText; ++$i) { + if (0 != ($i % 2)) { + continue; + } + $tmpText = $data[$i]['c']; + $decodedText = isset($currentFont) ? $currentFont->decodeOctal($tmpText) : $tmpText; + $decodedText = str_replace( + ['\\\\', '\(', '\)', '\n', '\r', '\t', '\ '], + ['\\', '(', ')', "\n", "\r", "\t", ' '], + $decodedText + ); + + $decodedText = mb_convert_encoding($decodedText, 'UTF-8', 'ISO-8859-1'); + + if (isset($currentFont)) { + $decodedText = $currentFont->decodeContent($decodedText); + } + $command['c'][$i]['c'] = $decodedText; + continue; + } + } elseif ('Tf' == $command['o'] || 'TF' == $command['o']) { + $fontId = explode(' ', $command['c'])[0]; + // If document is a FPDI/FPDF the $page has the correct font + $currentFont = isset($fpdfPage) ? $fpdfPage->getFont($fontId) : $this->getFont($fontId); + continue; + } elseif ('Q' == $command['o']) { + $currentFont = $clippedFont; + } elseif ('q' == $command['o']) { + $clippedFont = $currentFont; + } + } + + return $extractedRawData; + } + + /** + * Gets just the Text commands that are involved in text positions and + * Text Matrix (Tm) + * + * It extract just the PDF commands that are involved with text positions, and + * the Text Matrix (Tm). These are: BT, ET, TL, Td, TD, Tm, T*, Tj, ', ", and TJ + * + * @param array $extractedDecodedRawData The data extracted by extractDecodeRawData. + * If it is null, the method extractDecodeRawData is called. + * + * @return array An array with the text command of the page + */ + public function getDataCommands(array $extractedDecodedRawData = null): array + { + if (!isset($extractedDecodedRawData) || !$extractedDecodedRawData) { + $extractedDecodedRawData = $this->extractDecodedRawData(); + } + $extractedData = []; + foreach ($extractedDecodedRawData as $command) { + switch ($command['o']) { + /* + * BT + * Begin a text object, inicializind the Tm and Tlm to identity matrix + */ + case 'BT': + $extractedData[] = $command; + break; + + /* + * ET + * End a text object, discarding the text matrix + */ + case 'ET': + $extractedData[] = $command; + break; + + /* + * leading TL + * Set the text leading, Tl, to leading. Tl is used by the T*, ' and " operators. + * Initial value: 0 + */ + case 'TL': + $extractedData[] = $command; + break; + + /* + * tx ty Td + * Move to the start of the next line, offset form the start of the + * current line by tx, ty. + */ + case 'Td': + $extractedData[] = $command; + break; + + /* + * tx ty TD + * Move to the start of the next line, offset form the start of the + * current line by tx, ty. As a side effect, this operator set the leading + * parameter in the text state. This operator has the same effect as the + * code: + * -ty TL + * tx ty Td + */ + case 'TD': + $extractedData[] = $command; + break; + + /* + * a b c d e f Tm + * Set the text matrix, Tm, and the text line matrix, Tlm. The operands are + * all numbers, and the initial value for Tm and Tlm is the identity matrix + * [1 0 0 1 0 0] + */ + case 'Tm': + $extractedData[] = $command; + break; + + /* + * T* + * Move to the start of the next line. This operator has the same effect + * as the code: + * 0 Tl Td + * Where Tl is the current leading parameter in the text state. + */ + case 'T*': + $extractedData[] = $command; + break; + + /* + * string Tj + * Show a Text String + */ + case 'Tj': + $extractedData[] = $command; + break; + + /* + * string ' + * Move to the next line and show a text string. This operator has the + * same effect as the code: + * T* + * string Tj + */ + case "'": + $extractedData[] = $command; + break; + + /* + * aw ac string " + * Move to the next lkine and show a text string, using aw as the word + * spacing and ac as the character spacing. This operator has the same + * effect as the code: + * aw Tw + * ac Tc + * string ' + * Tw set the word spacing, Tw, to wordSpace. + * Tc Set the character spacing, Tc, to charsSpace. + */ + case '"': + $extractedData[] = $command; + break; + + case 'Tf': + case 'TF': + $extractedData[] = $command; + break; + + /* + * array TJ + * Show one or more text strings allow individual glyph positioning. + * Each lement of array con be a string or a number. If the element is + * a string, this operator shows the string. If it is a number, the + * operator adjust the text position by that amount; that is, it translates + * the text matrix, Tm. This amount is substracted form the current + * horizontal or vertical coordinate, depending on the writing mode. + * in the default coordinate system, a positive adjustment has the effect + * of moving the next glyph painted either to the left or down by the given + * amount. + */ + case 'TJ': + $extractedData[] = $command; + break; + default: + } + } + + return $extractedData; + } + + /** + * Gets the Text Matrix of the text in the page + * + * Return an array where every item is an array where the first item is the + * Text Matrix (Tm) and the second is a string with the text data. The Text matrix + * is an array of 6 numbers. The last 2 numbers are the coordinates X and Y of the + * text. The first 4 numbers has to be with Scalation, Rotation and Skew of the text. + * + * @param array $dataCommands the data extracted by getDataCommands + * if null getDataCommands is called + * + * @return array an array with the data of the page including the Tm information + * of any text in the page + */ + public function getDataTm(array $dataCommands = null): array + { + if (!isset($dataCommands) || !$dataCommands) { + $dataCommands = $this->getDataCommands(); + } + + /* + * At the beginning of a text object Tm is the identity matrix + */ + $defaultTm = ['1', '0', '0', '1', '0', '0']; + + /* + * Set the text leading used by T*, ' and " operators + */ + $defaultTl = 0; + + /* + * Set default values for font data + */ + $defaultFontId = -1; + $defaultFontSize = 1; + + /* + * Indexes of horizontal/vertical scaling and X,Y-coordinates in the matrix (Tm) + */ + $hSc = 0; // horizontal scaling + /** + * index of vertical scaling in the array that encodes the text matrix. + * for more information: https://github.com/smalot/pdfparser/pull/559#discussion_r1053415500 + */ + $vSc = 3; + $x = 4; + $y = 5; + + /* + * x,y-coordinates of text space origin in user units + * + * These will be assigned the value of the currently printed string + */ + $Tx = 0; + $Ty = 0; + + $Tm = $defaultTm; + $Tl = $defaultTl; + $fontId = $defaultFontId; + $fontSize = $defaultFontSize; // reflects fontSize set by Tf or Tfs + + $extractedTexts = $this->getTextArray(); + $extractedData = []; + foreach ($dataCommands as $command) { + $currentText = $extractedTexts[\count($extractedData)]; + switch ($command['o']) { + /* + * BT + * Begin a text object, initializing the Tm and Tlm to identity matrix + */ + case 'BT': + $Tm = $defaultTm; + $Tl = $defaultTl; + $Tx = 0; + $Ty = 0; + $fontId = $defaultFontId; + $fontSize = $defaultFontSize; + break; + + /* + * ET + * End a text object, discarding the text matrix + */ + case 'ET': + $Tm = $defaultTm; + $Tl = $defaultTl; + $Tx = 0; + $Ty = 0; + $fontId = $defaultFontId; + $fontSize = $defaultFontSize; + break; + + /* + * text leading TL + * Set the text leading, Tl, to leading. Tl is used by the T*, ' and " operators. + * Initial value: 0 + */ + case 'TL': + // scaled text leading + $Tl = (float) $command['c'] * (float) $Tm[$vSc]; + break; + + /* + * tx ty Td + * Move to the start of the next line, offset form the start of the + * current line by tx, ty. + */ + case 'Td': + $coord = explode(' ', $command['c']); + $Tx += (float) $coord[0] * (float) $Tm[$hSc]; + $Ty += (float) $coord[1] * (float) $Tm[$vSc]; + $Tm[$x] = (string) $Tx; + $Tm[$y] = (string) $Ty; + break; + + /* + * tx ty TD + * Move to the start of the next line, offset form the start of the + * current line by tx, ty. As a side effect, this operator set the leading + * parameter in the text state. This operator has the same effect as the + * code: + * -ty TL + * tx ty Td + */ + case 'TD': + $coord = explode(' ', $command['c']); + $Tl = -((float) $coord[1] * (float) $Tm[$vSc]); + $Tx += (float) $coord[0] * (float) $Tm[$hSc]; + $Ty += (float) $coord[1] * (float) $Tm[$vSc]; + $Tm[$x] = (string) $Tx; + $Tm[$y] = (string) $Ty; + break; + + /* + * a b c d e f Tm + * Set the text matrix, Tm, and the text line matrix, Tlm. The operands are + * all numbers, and the initial value for Tm and Tlm is the identity matrix + * [1 0 0 1 0 0] + */ + case 'Tm': + $Tm = explode(' ', $command['c']); + $Tx = (float) $Tm[$x]; + $Ty = (float) $Tm[$y]; + break; + + /* + * T* + * Move to the start of the next line. This operator has the same effect + * as the code: + * 0 Tl Td + * Where Tl is the current leading parameter in the text state. + */ + case 'T*': + $Ty -= $Tl; + $Tm[$y] = (string) $Ty; + break; + + /* + * string Tj + * Show a Text String + */ + case 'Tj': + $data = [$Tm, $currentText]; + if ($this->config->getDataTmFontInfoHasToBeIncluded()) { + $data[] = $fontId; + $data[] = $fontSize; + } + $extractedData[] = $data; + break; + + /* + * string ' + * Move to the next line and show a text string. This operator has the + * same effect as the code: + * T* + * string Tj + */ + case "'": + $Ty -= $Tl; + $Tm[$y] = (string) $Ty; + $extractedData[] = [$Tm, $currentText]; + break; + + /* + * aw ac string " + * Move to the next line and show a text string, using aw as the word + * spacing and ac as the character spacing. This operator has the same + * effect as the code: + * aw Tw + * ac Tc + * string ' + * Tw set the word spacing, Tw, to wordSpace. + * Tc Set the character spacing, Tc, to charsSpace. + */ + case '"': + $data = explode(' ', $currentText); + $Ty -= $Tl; + $Tm[$y] = (string) $Ty; + $extractedData[] = [$Tm, $data[2]]; // Verify + break; + + case 'Tf': + /* + * From PDF 1.0 specification, page 106: + * fontname size Tf Set font and size + * Sets the text font and text size in the graphics state. There is no default value for + * either fontname or size; they must be selected using Tf before drawing any text. + * fontname is a resource name. size is a number expressed in text space units. + * + * Source: https://ia902503.us.archive.org/10/items/pdfy-0vt8s-egqFwDl7L2/PDF%20Reference%201.0.pdf + * Introduced with https://github.com/smalot/pdfparser/pull/516 + */ + list($fontId, $fontSize) = explode(' ', $command['c'], 2); + break; + + /* + * array TJ + * Show one or more text strings allow individual glyph positioning. + * Each lement of array con be a string or a number. If the element is + * a string, this operator shows the string. If it is a number, the + * operator adjust the text position by that amount; that is, it translates + * the text matrix, Tm. This amount is substracted form the current + * horizontal or vertical coordinate, depending on the writing mode. + * in the default coordinate system, a positive adjustment has the effect + * of moving the next glyph painted either to the left or down by the given + * amount. + */ + case 'TJ': + $data = [$Tm, $currentText]; + if ($this->config->getDataTmFontInfoHasToBeIncluded()) { + $data[] = $fontId; + $data[] = $fontSize; + } + $extractedData[] = $data; + break; + default: + } + } + $this->dataTm = $extractedData; + + return $extractedData; + } + + /** + * Gets text data that are around the given coordinates (X,Y) + * + * If the text is in near the given coordinates (X,Y) (or the TM info), + * the text is returned. The extractedData return by getDataTm, could be use to see + * where is the coordinates of a given text, using the TM info for it. + * + * @param float $x The X value of the coordinate to search for. if null + * just the Y value is considered (same Row) + * @param float $y The Y value of the coordinate to search for + * just the X value is considered (same column) + * @param float $xError The value less or more to consider an X to be "near" + * @param float $yError The value less or more to consider an Y to be "near" + * + * @return array An array of text that are near the given coordinates. If no text + * "near" the x,y coordinate, an empty array is returned. If Both, x + * and y coordinates are null, null is returned. + */ + public function getTextXY(float $x = null, float $y = null, float $xError = 0, float $yError = 0): array + { + if (!isset($this->dataTm) || !$this->dataTm) { + $this->getDataTm(); + } + + if (null !== $x) { + $x = (float) $x; + } + + if (null !== $y) { + $y = (float) $y; + } + + if (null === $x && null === $y) { + return []; + } + + $xError = (float) $xError; + $yError = (float) $yError; + + $extractedData = []; + foreach ($this->dataTm as $item) { + $tm = $item[0]; + $xTm = (float) $tm[4]; + $yTm = (float) $tm[5]; + $text = $item[1]; + if (null === $y) { + if (($xTm >= ($x - $xError)) && + ($xTm <= ($x + $xError))) { + $extractedData[] = [$tm, $text]; + continue; + } + } + if (null === $x) { + if (($yTm >= ($y - $yError)) && + ($yTm <= ($y + $yError))) { + $extractedData[] = [$tm, $text]; + continue; + } + } + if (($xTm >= ($x - $xError)) && + ($xTm <= ($x + $xError)) && + ($yTm >= ($y - $yError)) && + ($yTm <= ($y + $yError))) { + $extractedData[] = [$tm, $text]; + continue; + } + } + + return $extractedData; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Pages.php b/orcinus/pdfparser/src/Smalot/PdfParser/Pages.php new file mode 100644 index 0000000..6b87865 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Pages.php @@ -0,0 +1,73 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser; + +use Smalot\PdfParser\Element\ElementArray; + +/** + * Class Pages + */ +class Pages extends PDFObject +{ + /** + * @todo Objects other than Pages or Page might need to be treated specifically in order to get Page objects out of them, + * + * @see https://github.com/smalot/pdfparser/issues/331 + */ + public function getPages(bool $deep = false): array + { + if (!$this->has('Kids')) { + return []; + } + + /** @var ElementArray $kidsElement */ + $kidsElement = $this->get('Kids'); + + if (!$deep) { + return $kidsElement->getContent(); + } + + $kids = $kidsElement->getContent(); + $pages = []; + + foreach ($kids as $kid) { + if ($kid instanceof self) { + $pages = array_merge($pages, $kid->getPages(true)); + } elseif ($kid instanceof Page) { + $pages[] = $kid; + } + } + + return $pages; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/Parser.php b/orcinus/pdfparser/src/Smalot/PdfParser/Parser.php new file mode 100644 index 0000000..2109c81 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/Parser.php @@ -0,0 +1,327 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser; + +use Smalot\PdfParser\Element\ElementArray; +use Smalot\PdfParser\Element\ElementBoolean; +use Smalot\PdfParser\Element\ElementDate; +use Smalot\PdfParser\Element\ElementHexa; +use Smalot\PdfParser\Element\ElementName; +use Smalot\PdfParser\Element\ElementNull; +use Smalot\PdfParser\Element\ElementNumeric; +use Smalot\PdfParser\Element\ElementString; +use Smalot\PdfParser\Element\ElementXRef; +use Smalot\PdfParser\RawData\RawDataParser; + +/** + * Class Parser + */ +class Parser +{ + /** + * @var Config + */ + private $config; + + /** + * @var PDFObject[] + */ + protected $objects = []; + + protected $rawDataParser; + + public function __construct($cfg = [], ?Config $config = null) + { + $this->config = $config ?: new Config(); + $this->rawDataParser = new RawDataParser($cfg, $this->config); + } + + public function getConfig(): Config + { + return $this->config; + } + + /** + * @throws \Exception + */ + public function parseFile(string $filename): Document + { + $content = file_get_contents($filename); + /* + * 2018/06/20 @doganoo as multiple times a + * users have complained that the parseFile() + * method dies silently, it is an better option + * to remove the error control operator (@) and + * let the users know that the method throws an exception + * by adding @throws tag to PHPDoc. + * + * See here for an example: https://github.com/smalot/pdfparser/issues/204 + */ + return $this->parseContent($content); + } + + /** + * @param string $content PDF content to parse + * + * @throws \Exception if secured PDF file was detected + * @throws \Exception if no object list was found + */ + public function parseContent(string $content): Document + { + // Create structure from raw data. + list($xref, $data) = $this->rawDataParser->parseData($content); + + if (isset($xref['trailer']['encrypt'])) { + throw new \Exception('Secured pdf file are currently not supported.'); + } + + if (empty($data)) { + throw new \Exception('Object list not found. Possible secured file.'); + } + + // Create destination object. + $document = new Document(); + $this->objects = []; + + foreach ($data as $id => $structure) { + $this->parseObject($id, $structure, $document); + unset($data[$id]); + } + + $document->setTrailer($this->parseTrailer($xref['trailer'], $document)); + $document->setObjects($this->objects); + + return $document; + } + + protected function parseTrailer(array $structure, ?Document $document) + { + $trailer = []; + + foreach ($structure as $name => $values) { + $name = ucfirst($name); + + if (is_numeric($values)) { + $trailer[$name] = new ElementNumeric($values); + } elseif (\is_array($values)) { + $value = $this->parseTrailer($values, null); + $trailer[$name] = new ElementArray($value, null); + } elseif (false !== strpos($values, '_')) { + $trailer[$name] = new ElementXRef($values, $document); + } else { + $trailer[$name] = $this->parseHeaderElement('(', $values, $document); + } + } + + return new Header($trailer, $document); + } + + protected function parseObject(string $id, array $structure, ?Document $document) + { + $header = new Header([], $document); + $content = ''; + + foreach ($structure as $position => $part) { + if (\is_int($part)) { + $part = [null, null]; + } + switch ($part[0]) { + case '[': + $elements = []; + + foreach ($part[1] as $sub_element) { + $sub_type = $sub_element[0]; + $sub_value = $sub_element[1]; + $elements[] = $this->parseHeaderElement($sub_type, $sub_value, $document); + } + + $header = new Header($elements, $document); + break; + + case '<<': + $header = $this->parseHeader($part[1], $document); + break; + + case 'stream': + $content = isset($part[3][0]) ? $part[3][0] : $part[1]; + + if ($header->get('Type')->equals('ObjStm')) { + $match = []; + + // Split xrefs and contents. + preg_match('/^((\d+\s+\d+\s*)*)(.*)$/s', $content, $match); + $content = $match[3]; + + // Extract xrefs. + $xrefs = preg_split( + '/(\d+\s+\d+\s*)/s', + $match[1], + -1, + \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE + ); + $table = []; + + foreach ($xrefs as $xref) { + list($id, $position) = preg_split("/\s+/", trim($xref)); + $table[$position] = $id; + } + + ksort($table); + + $ids = array_values($table); + $positions = array_keys($table); + + foreach ($positions as $index => $position) { + $id = $ids[$index].'_0'; + $next_position = isset($positions[$index + 1]) ? $positions[$index + 1] : \strlen($content); + $sub_content = substr($content, $position, (int) $next_position - (int) $position); + + $sub_header = Header::parse($sub_content, $document); + $object = PDFObject::factory($document, $sub_header, '', $this->config); + $this->objects[$id] = $object; + } + + // It is not necessary to store this content. + + return; + } + break; + + default: + if ('null' != $part) { + $element = $this->parseHeaderElement($part[0], $part[1], $document); + + if ($element) { + $header = new Header([$element], $document); + } + } + break; + } + } + + if (!isset($this->objects[$id])) { + $this->objects[$id] = PDFObject::factory($document, $header, $content, $this->config); + } + } + + /** + * @throws \Exception + */ + protected function parseHeader(array $structure, ?Document $document): Header + { + $elements = []; + $count = \count($structure); + + for ($position = 0; $position < $count; $position += 2) { + $name = $structure[$position][1]; + $type = $structure[$position + 1][0]; + $value = $structure[$position + 1][1]; + + $elements[$name] = $this->parseHeaderElement($type, $value, $document); + } + + return new Header($elements, $document); + } + + /** + * @param string|array $value + * + * @return Element|Header|null + * + * @throws \Exception + */ + protected function parseHeaderElement(?string $type, $value, ?Document $document) + { + $valueIsEmpty = null == $value || '' == $value || false == $value; + if (('<<' === $type || '>>' === $type) && $valueIsEmpty) { + $value = []; + } + + switch ($type) { + case '<<': + case '>>': + $header = $this->parseHeader($value, $document); + PDFObject::factory($document, $header, null, $this->config); + + return $header; + + case 'numeric': + return new ElementNumeric($value); + + case 'boolean': + return new ElementBoolean($value); + + case 'null': + return new ElementNull(); + + case '(': + if ($date = ElementDate::parse('('.$value.')', $document)) { + return $date; + } + + return ElementString::parse('('.$value.')', $document); + + case '<': + return $this->parseHeaderElement('(', ElementHexa::decode($value), $document); + + case '/': + return ElementName::parse('/'.$value, $document); + + case 'ojbref': // old mistake in tcpdf parser + case 'objref': + return new ElementXRef($value, $document); + + case '[': + $values = []; + + if (\is_array($value)) { + foreach ($value as $sub_element) { + $sub_type = $sub_element[0]; + $sub_value = $sub_element[1]; + $values[] = $this->parseHeaderElement($sub_type, $sub_value, $document); + } + } + + return new ElementArray($values, $document); + + case 'endstream': + case 'obj': // I don't know what it means but got my project fixed. + case '': + // Nothing to do with. + return null; + + default: + throw new \Exception('Invalid type: "'.$type.'".'); + } + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/RawData/FilterHelper.php b/orcinus/pdfparser/src/Smalot/PdfParser/RawData/FilterHelper.php new file mode 100644 index 0000000..2cb28dc --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/RawData/FilterHelper.php @@ -0,0 +1,396 @@ + + * + * @date 2020-01-06 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\RawData; + +class FilterHelper +{ + protected $availableFilters = ['ASCIIHexDecode', 'ASCII85Decode', 'LZWDecode', 'FlateDecode', 'RunLengthDecode']; + + /** + * Decode data using the specified filter type. + * + * @param string $filter Filter name + * @param string $data Data to decode + * + * @return string Decoded data string + * + * @throws \Exception if a certain decode function is not implemented yet + */ + public function decodeFilter(string $filter, string $data, int $decodeMemoryLimit = 0): string + { + switch ($filter) { + case 'ASCIIHexDecode': + return $this->decodeFilterASCIIHexDecode($data); + + case 'ASCII85Decode': + return $this->decodeFilterASCII85Decode($data); + + case 'LZWDecode': + return $this->decodeFilterLZWDecode($data); + + case 'FlateDecode': + return $this->decodeFilterFlateDecode($data, $decodeMemoryLimit); + + case 'RunLengthDecode': + return $this->decodeFilterRunLengthDecode($data); + + case 'CCITTFaxDecode': + throw new \Exception('Decode CCITTFaxDecode not implemented yet.'); + case 'JBIG2Decode': + throw new \Exception('Decode JBIG2Decode not implemented yet.'); + case 'DCTDecode': + throw new \Exception('Decode DCTDecode not implemented yet.'); + case 'JPXDecode': + throw new \Exception('Decode JPXDecode not implemented yet.'); + case 'Crypt': + throw new \Exception('Decode Crypt not implemented yet.'); + default: + return $data; + } + } + + /** + * ASCIIHexDecode + * + * Decodes data encoded in an ASCII hexadecimal representation, reproducing the original binary data. + * + * @param string $data Data to decode + * + * @return string data string + * + * @throws \Exception + */ + protected function decodeFilterASCIIHexDecode(string $data): string + { + // all white-space characters shall be ignored + $data = preg_replace('/[\s]/', '', $data); + // check for EOD character: GREATER-THAN SIGN (3Eh) + $eod = strpos($data, '>'); + if (false !== $eod) { + // remove EOD and extra data (if any) + $data = substr($data, 0, $eod); + $eod = true; + } + // get data length + $data_length = \strlen($data); + if (0 != ($data_length % 2)) { + // odd number of hexadecimal digits + if ($eod) { + // EOD shall behave as if a 0 (zero) followed the last digit + $data = substr($data, 0, -1).'0'.substr($data, -1); + } else { + throw new \Exception('decodeFilterASCIIHexDecode: invalid code'); + } + } + // check for invalid characters + if (preg_match('/[^a-fA-F\d]/', $data) > 0) { + throw new \Exception('decodeFilterASCIIHexDecode: invalid code'); + } + // get one byte of binary data for each pair of ASCII hexadecimal digits + $decoded = pack('H*', $data); + + return $decoded; + } + + /** + * ASCII85Decode + * + * Decodes data encoded in an ASCII base-85 representation, reproducing the original binary data. + * + * @param string $data Data to decode + * + * @return string data string + * + * @throws \Exception + */ + protected function decodeFilterASCII85Decode(string $data): string + { + // initialize string to return + $decoded = ''; + // all white-space characters shall be ignored + $data = preg_replace('/[\s]/', '', $data); + // remove start sequence 2-character sequence <~ (3Ch)(7Eh) + if (false !== strpos($data, '<~')) { + // remove EOD and extra data (if any) + $data = substr($data, 2); + } + // check for EOD: 2-character sequence ~> (7Eh)(3Eh) + $eod = strpos($data, '~>'); + if (false !== $eod) { + // remove EOD and extra data (if any) + $data = substr($data, 0, $eod); + } + // data length + $data_length = \strlen($data); + // check for invalid characters + if (preg_match('/[^\x21-\x75,\x74]/', $data) > 0) { + throw new \Exception('decodeFilterASCII85Decode: invalid code'); + } + // z sequence + $zseq = \chr(0).\chr(0).\chr(0).\chr(0); + // position inside a group of 4 bytes (0-3) + $group_pos = 0; + $tuple = 0; + $pow85 = [85 * 85 * 85 * 85, 85 * 85 * 85, 85 * 85, 85, 1]; + + // for each byte + for ($i = 0; $i < $data_length; ++$i) { + // get char value + $char = \ord($data[$i]); + if (122 == $char) { // 'z' + if (0 == $group_pos) { + $decoded .= $zseq; + } else { + throw new \Exception('decodeFilterASCII85Decode: invalid code'); + } + } else { + // the value represented by a group of 5 characters should never be greater than 2^32 - 1 + $tuple += (($char - 33) * $pow85[$group_pos]); + if (4 == $group_pos) { + $decoded .= \chr($tuple >> 24).\chr($tuple >> 16).\chr($tuple >> 8).\chr($tuple); + $tuple = 0; + $group_pos = 0; + } else { + ++$group_pos; + } + } + } + if ($group_pos > 1) { + $tuple += $pow85[$group_pos - 1]; + } + // last tuple (if any) + switch ($group_pos) { + case 4: + $decoded .= \chr($tuple >> 24).\chr($tuple >> 16).\chr($tuple >> 8); + break; + + case 3: + $decoded .= \chr($tuple >> 24).\chr($tuple >> 16); + break; + + case 2: + $decoded .= \chr($tuple >> 24); + break; + + case 1: + throw new \Exception('decodeFilterASCII85Decode: invalid code'); + } + + return $decoded; + } + + /** + * FlateDecode + * + * Decompresses data encoded using the zlib/deflate compression method, reproducing the original text or binary data. + * + * @param string $data Data to decode + * @param int $decodeMemoryLimit Memory limit on deflation + * + * @return string data string + * + * @throws \Exception + */ + protected function decodeFilterFlateDecode(string $data, int $decodeMemoryLimit): ?string + { + /* + * gzuncompress may throw a not catchable E_WARNING in case of an error (like $data is empty) + * the following set_error_handler changes an E_WARNING to an E_ERROR, which is catchable. + */ + set_error_handler(function ($errNo, $errStr) { + if (\E_WARNING === $errNo) { + throw new \Exception($errStr); + } else { + // fallback to default php error handler + return false; + } + }); + + $decoded = null; + + // initialize string to return + try { + $decoded = gzuncompress($data, $decodeMemoryLimit); + if (false === $decoded) { + throw new \Exception('decodeFilterFlateDecode: invalid code'); + } + } catch (\Exception $e) { + throw $e; + } finally { + // Restore old handler just in case it was customized outside of PDFParser. + restore_error_handler(); + } + + return $decoded; + } + + /** + * LZWDecode + * + * Decompresses data encoded using the LZW (Lempel-Ziv-Welch) adaptive compression method, reproducing the original text or binary data. + * + * @param string $data Data to decode + * + * @return string Data string + */ + protected function decodeFilterLZWDecode(string $data): string + { + // initialize string to return + $decoded = ''; + // data length + $data_length = \strlen($data); + // convert string to binary string + $bitstring = ''; + for ($i = 0; $i < $data_length; ++$i) { + $bitstring .= sprintf('%08b', \ord($data[$i])); + } + // get the number of bits + $data_length = \strlen($bitstring); + // initialize code length in bits + $bitlen = 9; + // initialize dictionary index + $dix = 258; + // initialize the dictionary (with the first 256 entries). + $dictionary = []; + for ($i = 0; $i < 256; ++$i) { + $dictionary[$i] = \chr($i); + } + // previous val + $prev_index = 0; + // while we encounter EOD marker (257), read code_length bits + while (($data_length > 0) && (257 != ($index = bindec(substr($bitstring, 0, $bitlen))))) { + // remove read bits from string + $bitstring = substr($bitstring, $bitlen); + // update number of bits + $data_length -= $bitlen; + if (256 == $index) { // clear-table marker + // reset code length in bits + $bitlen = 9; + // reset dictionary index + $dix = 258; + $prev_index = 256; + // reset the dictionary (with the first 256 entries). + $dictionary = []; + for ($i = 0; $i < 256; ++$i) { + $dictionary[$i] = \chr($i); + } + } elseif (256 == $prev_index) { + // first entry + $decoded .= $dictionary[$index]; + $prev_index = $index; + } else { + // check if index exist in the dictionary + if ($index < $dix) { + // index exist on dictionary + $decoded .= $dictionary[$index]; + $dic_val = $dictionary[$prev_index].$dictionary[$index][0]; + // store current index + $prev_index = $index; + } else { + // index do not exist on dictionary + $dic_val = $dictionary[$prev_index].$dictionary[$prev_index][0]; + $decoded .= $dic_val; + } + // update dictionary + $dictionary[$dix] = $dic_val; + ++$dix; + // change bit length by case + if (2047 == $dix) { + $bitlen = 12; + } elseif (1023 == $dix) { + $bitlen = 11; + } elseif (511 == $dix) { + $bitlen = 10; + } + } + } + + return $decoded; + } + + /** + * RunLengthDecode + * + * Decompresses data encoded using a byte-oriented run-length encoding algorithm. + * + * @param string $data Data to decode + */ + protected function decodeFilterRunLengthDecode(string $data): string + { + // initialize string to return + $decoded = ''; + // data length + $data_length = \strlen($data); + $i = 0; + while ($i < $data_length) { + // get current byte value + $byte = \ord($data[$i]); + if (128 == $byte) { + // a length value of 128 denote EOD + break; + } elseif ($byte < 128) { + // if the length byte is in the range 0 to 127 + // the following length + 1 (1 to 128) bytes shall be copied literally during decompression + $decoded .= substr($data, $i + 1, $byte + 1); + // move to next block + $i += ($byte + 2); + } else { + // if length is in the range 129 to 255, + // the following single byte shall be copied 257 - length (2 to 128) times during decompression + $decoded .= str_repeat($data[$i + 1], 257 - $byte); + // move to next block + $i += 2; + } + } + + return $decoded; + } + + /** + * @return array list of available filters + */ + public function getAvailableFilters(): array + { + return $this->availableFilters; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/RawData/RawDataParser.php b/orcinus/pdfparser/src/Smalot/PdfParser/RawData/RawDataParser.php new file mode 100644 index 0000000..71abbd8 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/RawData/RawDataParser.php @@ -0,0 +1,902 @@ + + * + * @date 2020-01-06 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\RawData; + +use Smalot\PdfParser\Config; + +class RawDataParser +{ + /** + * @var \Smalot\PdfParser\Config + */ + private $config; + + /** + * Configuration array. + */ + protected $cfg = [ + // if `true` ignore filter decoding errors + 'ignore_filter_decoding_errors' => true, + // if `true` ignore missing filter decoding errors + 'ignore_missing_filter_decoders' => true, + ]; + + protected $filterHelper; + protected $objects; + + /** + * @param array $cfg Configuration array, default is [] + */ + public function __construct($cfg = [], Config $config = null) + { + // merge given array with default values + $this->cfg = array_merge($this->cfg, $cfg); + + $this->filterHelper = new FilterHelper(); + $this->config = $config ?: new Config(); + } + + /** + * Decode the specified stream. + * + * @param string $pdfData PDF data + * @param array $sdic Stream's dictionary array + * @param string $stream Stream to decode + * + * @return array containing decoded stream data and remaining filters + * + * @throws \Exception + */ + protected function decodeStream(string $pdfData, array $xref, array $sdic, string $stream): array + { + // get stream length and filters + $slength = \strlen($stream); + if ($slength <= 0) { + return ['', []]; + } + $filters = []; + foreach ($sdic as $k => $v) { + if ('/' == $v[0]) { + if (('Length' == $v[1]) && (isset($sdic[$k + 1])) && ('numeric' == $sdic[$k + 1][0])) { + // get declared stream length + $declength = (int) $sdic[$k + 1][1]; + if ($declength < $slength) { + $stream = substr($stream, 0, $declength); + $slength = $declength; + } + } elseif (('Filter' == $v[1]) && (isset($sdic[$k + 1]))) { + // resolve indirect object + $objval = $this->getObjectVal($pdfData, $xref, $sdic[$k + 1]); + if ('/' == $objval[0]) { + // single filter + $filters[] = $objval[1]; + } elseif ('[' == $objval[0]) { + // array of filters + foreach ($objval[1] as $flt) { + if ('/' == $flt[0]) { + $filters[] = $flt[1]; + } + } + } + } + } + } + + // decode the stream + $remaining_filters = []; + foreach ($filters as $filter) { + if (\in_array($filter, $this->filterHelper->getAvailableFilters())) { + try { + $stream = $this->filterHelper->decodeFilter($filter, $stream, $this->config->getDecodeMemoryLimit()); + } catch (\Exception $e) { + $emsg = $e->getMessage(); + if ((('~' == $emsg[0]) && !$this->cfg['ignore_missing_filter_decoders']) + || (('~' != $emsg[0]) && !$this->cfg['ignore_filter_decoding_errors']) + ) { + throw new \Exception($e->getMessage()); + } + } + } else { + // add missing filter to array + $remaining_filters[] = $filter; + } + } + + return [$stream, $remaining_filters]; + } + + /** + * Decode the Cross-Reference section + * + * @param string $pdfData PDF data + * @param int $startxref Offset at which the xref section starts (position of the 'xref' keyword) + * @param array $xref Previous xref array (if any) + * + * @return array containing xref and trailer data + * + * @throws \Exception + */ + protected function decodeXref(string $pdfData, int $startxref, array $xref = []): array + { + $startxref += 4; // 4 is the length of the word 'xref' + // skip initial white space chars + $offset = $startxref + strspn($pdfData, $this->config->getPdfWhitespaces(), $startxref); + // initialize object number + $obj_num = 0; + // search for cross-reference entries or subsection + while (preg_match('/([0-9]+)[\x20]([0-9]+)[\x20]?([nf]?)(\r\n|[\x20]?[\r\n])/', $pdfData, $matches, \PREG_OFFSET_CAPTURE, $offset) > 0) { + if ($matches[0][1] != $offset) { + // we are on another section + break; + } + $offset += \strlen($matches[0][0]); + if ('n' == $matches[3][0]) { + // create unique object index: [object number]_[generation number] + $index = $obj_num.'_'.(int) $matches[2][0]; + // check if object already exist + if (!isset($xref['xref'][$index])) { + // store object offset position + $xref['xref'][$index] = (int) $matches[1][0]; + } + ++$obj_num; + } elseif ('f' == $matches[3][0]) { + ++$obj_num; + } else { + // object number (index) + $obj_num = (int) $matches[1][0]; + } + } + // get trailer data + if (preg_match('/trailer[\s]*<<(.*)>>/isU', $pdfData, $matches, \PREG_OFFSET_CAPTURE, $offset) > 0) { + $trailer_data = $matches[1][0]; + if (!isset($xref['trailer']) || empty($xref['trailer'])) { + // get only the last updated version + $xref['trailer'] = []; + // parse trailer_data + if (preg_match('/Size[\s]+([0-9]+)/i', $trailer_data, $matches) > 0) { + $xref['trailer']['size'] = (int) $matches[1]; + } + if (preg_match('/Root[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) > 0) { + $xref['trailer']['root'] = (int) $matches[1].'_'.(int) $matches[2]; + } + if (preg_match('/Encrypt[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) > 0) { + $xref['trailer']['encrypt'] = (int) $matches[1].'_'.(int) $matches[2]; + } + if (preg_match('/Info[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) > 0) { + $xref['trailer']['info'] = (int) $matches[1].'_'.(int) $matches[2]; + } + if (preg_match('/ID[\s]*[\[][\s]*[<]([^>]*)[>][\s]*[<]([^>]*)[>]/i', $trailer_data, $matches) > 0) { + $xref['trailer']['id'] = []; + $xref['trailer']['id'][0] = $matches[1]; + $xref['trailer']['id'][1] = $matches[2]; + } + } + if (preg_match('/Prev[\s]+([0-9]+)/i', $trailer_data, $matches) > 0) { + // get previous xref + $xref = $this->getXrefData($pdfData, (int) $matches[1], $xref); + } + } else { + throw new \Exception('Unable to find trailer'); + } + + return $xref; + } + + /** + * Decode the Cross-Reference Stream section + * + * @param string $pdfData PDF data + * @param int $startxref Offset at which the xref section starts + * @param array $xref Previous xref array (if any) + * + * @return array containing xref and trailer data + * + * @throws \Exception if unknown PNG predictor detected + */ + protected function decodeXrefStream(string $pdfData, int $startxref, array $xref = []): array + { + // try to read Cross-Reference Stream + $xrefobj = $this->getRawObject($pdfData, $startxref); + $xrefcrs = $this->getIndirectObject($pdfData, $xref, $xrefobj[1], $startxref, true); + if (!isset($xref['trailer']) || empty($xref['trailer'])) { + // get only the last updated version + $xref['trailer'] = []; + $filltrailer = true; + } else { + $filltrailer = false; + } + if (!isset($xref['xref'])) { + $xref['xref'] = []; + } + $valid_crs = false; + $columns = 0; + $predictor = null; + $sarr = $xrefcrs[0][1]; + if (!\is_array($sarr)) { + $sarr = []; + } + + $wb = []; + + foreach ($sarr as $k => $v) { + if ( + ('/' == $v[0]) + && ('Type' == $v[1]) + && ( + isset($sarr[$k + 1]) + && '/' == $sarr[$k + 1][0] + && 'XRef' == $sarr[$k + 1][1] + ) + ) { + $valid_crs = true; + } elseif (('/' == $v[0]) && ('Index' == $v[1]) && (isset($sarr[$k + 1]))) { + // initialize list for: first object number in the subsection / number of objects + $index_blocks = []; + for ($m = 0; $m < \count($sarr[$k + 1][1]); $m += 2) { + $index_blocks[] = [$sarr[$k + 1][1][$m][1], $sarr[$k + 1][1][$m + 1][1]]; + } + } elseif (('/' == $v[0]) && ('Prev' == $v[1]) && (isset($sarr[$k + 1]) && ('numeric' == $sarr[$k + 1][0]))) { + // get previous xref offset + $prevxref = (int) $sarr[$k + 1][1]; + } elseif (('/' == $v[0]) && ('W' == $v[1]) && (isset($sarr[$k + 1]))) { + // number of bytes (in the decoded stream) of the corresponding field + $wb[0] = (int) $sarr[$k + 1][1][0][1]; + $wb[1] = (int) $sarr[$k + 1][1][1][1]; + $wb[2] = (int) $sarr[$k + 1][1][2][1]; + } elseif (('/' == $v[0]) && ('DecodeParms' == $v[1]) && (isset($sarr[$k + 1][1]))) { + $decpar = $sarr[$k + 1][1]; + foreach ($decpar as $kdc => $vdc) { + if ( + '/' == $vdc[0] + && 'Columns' == $vdc[1] + && ( + isset($decpar[$kdc + 1]) + && 'numeric' == $decpar[$kdc + 1][0] + ) + ) { + $columns = (int) $decpar[$kdc + 1][1]; + } elseif ( + '/' == $vdc[0] + && 'Predictor' == $vdc[1] + && ( + isset($decpar[$kdc + 1]) + && 'numeric' == $decpar[$kdc + 1][0] + ) + ) { + $predictor = (int) $decpar[$kdc + 1][1]; + } + } + } elseif ($filltrailer) { + if (('/' == $v[0]) && ('Size' == $v[1]) && (isset($sarr[$k + 1]) && ('numeric' == $sarr[$k + 1][0]))) { + $xref['trailer']['size'] = $sarr[$k + 1][1]; + } elseif (('/' == $v[0]) && ('Root' == $v[1]) && (isset($sarr[$k + 1]) && ('objref' == $sarr[$k + 1][0]))) { + $xref['trailer']['root'] = $sarr[$k + 1][1]; + } elseif (('/' == $v[0]) && ('Info' == $v[1]) && (isset($sarr[$k + 1]) && ('objref' == $sarr[$k + 1][0]))) { + $xref['trailer']['info'] = $sarr[$k + 1][1]; + } elseif (('/' == $v[0]) && ('Encrypt' == $v[1]) && (isset($sarr[$k + 1]) && ('objref' == $sarr[$k + 1][0]))) { + $xref['trailer']['encrypt'] = $sarr[$k + 1][1]; + } elseif (('/' == $v[0]) && ('ID' == $v[1]) && (isset($sarr[$k + 1]))) { + $xref['trailer']['id'] = []; + $xref['trailer']['id'][0] = $sarr[$k + 1][1][0][1]; + $xref['trailer']['id'][1] = $sarr[$k + 1][1][1][1]; + } + } + } + + // decode data + if ($valid_crs && isset($xrefcrs[1][3][0])) { + if (null !== $predictor) { + // number of bytes in a row + $rowlen = ($columns + 1); + // convert the stream into an array of integers + /** @var array */ + $sdata = unpack('C*', $xrefcrs[1][3][0]); + // TODO: Handle the case when unpack returns false + + // split the rows + $sdata = array_chunk($sdata, $rowlen); + + // initialize decoded array + $ddata = []; + // initialize first row with zeros + $prev_row = array_fill(0, $rowlen, 0); + // for each row apply PNG unpredictor + foreach ($sdata as $k => $row) { + // initialize new row + $ddata[$k] = []; + // get PNG predictor value + $predictor = (10 + $row[0]); + // for each byte on the row + for ($i = 1; $i <= $columns; ++$i) { + // new index + $j = ($i - 1); + $row_up = $prev_row[$j]; + if (1 == $i) { + $row_left = 0; + $row_upleft = 0; + } else { + $row_left = $row[$i - 1]; + $row_upleft = $prev_row[$j - 1]; + } + switch ($predictor) { + case 10: // PNG prediction (on encoding, PNG None on all rows) + $ddata[$k][$j] = $row[$i]; + break; + + case 11: // PNG prediction (on encoding, PNG Sub on all rows) + $ddata[$k][$j] = (($row[$i] + $row_left) & 0xFF); + break; + + case 12: // PNG prediction (on encoding, PNG Up on all rows) + $ddata[$k][$j] = (($row[$i] + $row_up) & 0xFF); + break; + + case 13: // PNG prediction (on encoding, PNG Average on all rows) + $ddata[$k][$j] = (($row[$i] + (($row_left + $row_up) / 2)) & 0xFF); + break; + + case 14: // PNG prediction (on encoding, PNG Paeth on all rows) + // initial estimate + $p = ($row_left + $row_up - $row_upleft); + // distances + $pa = abs($p - $row_left); + $pb = abs($p - $row_up); + $pc = abs($p - $row_upleft); + $pmin = min($pa, $pb, $pc); + // return minimum distance + switch ($pmin) { + case $pa: + $ddata[$k][$j] = (($row[$i] + $row_left) & 0xFF); + break; + + case $pb: + $ddata[$k][$j] = (($row[$i] + $row_up) & 0xFF); + break; + + case $pc: + $ddata[$k][$j] = (($row[$i] + $row_upleft) & 0xFF); + break; + } + break; + + default: // PNG prediction (on encoding, PNG optimum) + throw new \Exception('Unknown PNG predictor: '.$predictor); + } + } + $prev_row = $ddata[$k]; + } // end for each row + // complete decoding + } else { + // number of bytes in a row + $rowlen = array_sum($wb); + // convert the stream into an array of integers + $sdata = unpack('C*', $xrefcrs[1][3][0]); + // split the rows + $ddata = array_chunk($sdata, $rowlen); + } + + $sdata = []; + + // for every row + foreach ($ddata as $k => $row) { + // initialize new row + $sdata[$k] = [0, 0, 0]; + if (0 == $wb[0]) { + // default type field + $sdata[$k][0] = 1; + } + $i = 0; // count bytes in the row + // for every column + for ($c = 0; $c < 3; ++$c) { + // for every byte on the column + for ($b = 0; $b < $wb[$c]; ++$b) { + if (isset($row[$i])) { + $sdata[$k][$c] += ($row[$i] << (($wb[$c] - 1 - $b) * 8)); + } + ++$i; + } + } + } + + // fill xref + if (isset($index_blocks)) { + // load the first object number of the first /Index entry + $obj_num = $index_blocks[0][0]; + } else { + $obj_num = 0; + } + foreach ($sdata as $k => $row) { + switch ($row[0]) { + case 0: // (f) linked list of free objects + break; + + case 1: // (n) objects that are in use but are not compressed + // create unique object index: [object number]_[generation number] + $index = $obj_num.'_'.$row[2]; + // check if object already exist + if (!isset($xref['xref'][$index])) { + // store object offset position + $xref['xref'][$index] = $row[1]; + } + break; + + case 2: // compressed objects + // $row[1] = object number of the object stream in which this object is stored + // $row[2] = index of this object within the object stream + $index = $row[1].'_0_'.$row[2]; + $xref['xref'][$index] = -1; + break; + + default: // null objects + break; + } + ++$obj_num; + if (isset($index_blocks)) { + // reduce the number of remaining objects + --$index_blocks[0][1]; + if (0 == $index_blocks[0][1]) { + // remove the actual used /Index entry + array_shift($index_blocks); + if (0 < \count($index_blocks)) { + // load the first object number of the following /Index entry + $obj_num = $index_blocks[0][0]; + } else { + // if there are no more entries, remove $index_blocks to avoid actions on an empty array + unset($index_blocks); + } + } + } + } + } // end decoding data + if (isset($prevxref)) { + // get previous xref + $xref = $this->getXrefData($pdfData, $prevxref, $xref); + } + + return $xref; + } + + protected function getObjectHeaderPattern(array $objRefs): string + { + // consider all whitespace character (PDF specifications) + return '/'.$objRefs[0].$this->config->getPdfWhitespacesRegex().$objRefs[1].$this->config->getPdfWhitespacesRegex().'obj/'; + } + + protected function getObjectHeaderLen(array $objRefs): int + { + // "4 0 obj" + // 2 whitespaces + strlen("obj") = 5 + return 5 + \strlen($objRefs[0]) + \strlen($objRefs[1]); + } + + /** + * Get content of indirect object. + * + * @param string $pdfData PDF data + * @param string $objRef Object number and generation number separated by underscore character + * @param int $offset Object offset + * @param bool $decoding If true decode streams + * + * @return array containing object data + * + * @throws \Exception if invalid object reference found + */ + protected function getIndirectObject(string $pdfData, array $xref, string $objRef, int $offset = 0, bool $decoding = true): array + { + /* + * build indirect object header + */ + // $objHeader = "[object number] [generation number] obj" + $objRefArr = explode('_', $objRef); + if (2 !== \count($objRefArr)) { + throw new \Exception('Invalid object reference for $obj.'); + } + + $objHeaderLen = $this->getObjectHeaderLen($objRefArr); + + /* + * check if we are in position + */ + // ignore whitespace characters at offset + $offset += strspn($pdfData, $this->config->getPdfWhitespaces(), $offset); + // ignore leading zeros for object number + $offset += strspn($pdfData, '0', $offset); + if (0 == preg_match($this->getObjectHeaderPattern($objRefArr), substr($pdfData, $offset, $objHeaderLen))) { + // an indirect reference to an undefined object shall be considered a reference to the null object + return ['null', 'null', $offset]; + } + + /* + * get content + */ + // starting position of object content + $offset += $objHeaderLen; + $objContentArr = []; + $i = 0; // object main index + do { + $oldOffset = $offset; + // get element + $element = $this->getRawObject($pdfData, $offset); + $offset = $element[2]; + // decode stream using stream's dictionary information + if ($decoding && ('stream' === $element[0]) && (isset($objContentArr[$i - 1][0])) && ('<<' === $objContentArr[$i - 1][0])) { + $element[3] = $this->decodeStream($pdfData, $xref, $objContentArr[$i - 1][1], $element[1]); + } + $objContentArr[$i] = $element; + ++$i; + } while (('endobj' !== $element[0]) && ($offset !== $oldOffset)); + // remove closing delimiter + array_pop($objContentArr); + + /* + * return raw object content + */ + return $objContentArr; + } + + /** + * Get the content of object, resolving indirect object reference if necessary. + * + * @param string $pdfData PDF data + * @param array $obj Object value + * + * @return array containing object data + * + * @throws \Exception + */ + protected function getObjectVal(string $pdfData, $xref, array $obj): array + { + if ('objref' == $obj[0]) { + // reference to indirect object + if (isset($this->objects[$obj[1]])) { + // this object has been already parsed + return $this->objects[$obj[1]]; + } elseif (isset($xref[$obj[1]])) { + // parse new object + $this->objects[$obj[1]] = $this->getIndirectObject($pdfData, $xref, $obj[1], $xref[$obj[1]], false); + + return $this->objects[$obj[1]]; + } + } + + return $obj; + } + + /** + * Get object type, raw value and offset to next object + * + * @param int $offset Object offset + * + * @return array containing object type, raw value and offset to next object + */ + protected function getRawObject(string $pdfData, int $offset = 0): array + { + $objtype = ''; // object type to be returned + $objval = ''; // object value to be returned + + // skip initial white space chars + $offset += strspn($pdfData, $this->config->getPdfWhitespaces(), $offset); + + // get first char + $char = $pdfData[$offset]; + // get object type + switch ($char) { + case '%': // \x25 PERCENT SIGN + // skip comment and search for next token + $next = strcspn($pdfData, "\r\n", $offset); + if ($next > 0) { + $offset += $next; + + return $this->getRawObject($pdfData, $offset); + } + break; + + case '/': // \x2F SOLIDUS + // name object + $objtype = $char; + ++$offset; + $span = strcspn($pdfData, "\x00\x09\x0a\x0c\x0d\x20\n\t\r\v\f\x28\x29\x3c\x3e\x5b\x5d\x7b\x7d\x2f\x25", $offset, 256); + if ($span > 0) { + $objval = substr($pdfData, $offset, $span); // unescaped value + $offset += $span; + } + break; + + case '(': // \x28 LEFT PARENTHESIS + case ')': // \x29 RIGHT PARENTHESIS + // literal string object + $objtype = $char; + ++$offset; + $strpos = $offset; + if ('(' == $char) { + $open_bracket = 1; + while ($open_bracket > 0) { + if (!isset($pdfData[$strpos])) { + break; + } + $ch = $pdfData[$strpos]; + switch ($ch) { + case '\\': // REVERSE SOLIDUS (5Ch) (Backslash) + // skip next character + ++$strpos; + break; + + case '(': // LEFT PARENHESIS (28h) + ++$open_bracket; + break; + + case ')': // RIGHT PARENTHESIS (29h) + --$open_bracket; + break; + } + ++$strpos; + } + $objval = substr($pdfData, $offset, $strpos - $offset - 1); + $offset = $strpos; + } + break; + + case '[': // \x5B LEFT SQUARE BRACKET + case ']': // \x5D RIGHT SQUARE BRACKET + // array object + $objtype = $char; + ++$offset; + if ('[' == $char) { + // get array content + $objval = []; + do { + $oldOffset = $offset; + // get element + $element = $this->getRawObject($pdfData, $offset); + $offset = $element[2]; + $objval[] = $element; + } while ((']' != $element[0]) && ($offset != $oldOffset)); + // remove closing delimiter + array_pop($objval); + } + break; + + case '<': // \x3C LESS-THAN SIGN + case '>': // \x3E GREATER-THAN SIGN + if (isset($pdfData[$offset + 1]) && ($pdfData[$offset + 1] == $char)) { + // dictionary object + $objtype = $char.$char; + $offset += 2; + if ('<' == $char) { + // get array content + $objval = []; + do { + $oldOffset = $offset; + // get element + $element = $this->getRawObject($pdfData, $offset); + $offset = $element[2]; + $objval[] = $element; + } while (('>>' != $element[0]) && ($offset != $oldOffset)); + // remove closing delimiter + array_pop($objval); + } + } else { + // hexadecimal string object + $objtype = $char; + ++$offset; + + $span = strspn($pdfData, "0123456789abcdefABCDEF\x09\x0a\x0c\x0d\x20", $offset); + $dataToCheck = $pdfData[$offset + $span] ?? null; + if ('<' == $char && $span > 0 && '>' == $dataToCheck) { + // remove white space characters + $objval = strtr(substr($pdfData, $offset, $span), $this->config->getPdfWhitespaces(), ''); + $offset += $span + 1; + } elseif (false !== ($endpos = strpos($pdfData, '>', $offset))) { + $offset = $endpos + 1; + } + } + break; + + default: + if ('endobj' == substr($pdfData, $offset, 6)) { + // indirect object + $objtype = 'endobj'; + $offset += 6; + } elseif ('null' == substr($pdfData, $offset, 4)) { + // null object + $objtype = 'null'; + $offset += 4; + $objval = 'null'; + } elseif ('true' == substr($pdfData, $offset, 4)) { + // boolean true object + $objtype = 'boolean'; + $offset += 4; + $objval = 'true'; + } elseif ('false' == substr($pdfData, $offset, 5)) { + // boolean false object + $objtype = 'boolean'; + $offset += 5; + $objval = 'false'; + } elseif ('stream' == substr($pdfData, $offset, 6)) { + // start stream object + $objtype = 'stream'; + $offset += 6; + if (1 == preg_match('/^([\r]?[\n])/isU', substr($pdfData, $offset, 4), $matches)) { + $offset += \strlen($matches[0]); + $pregResult = preg_match( + '/(endstream)[\x09\x0a\x0c\x0d\x20]/isU', + $pdfData, + $matches, + \PREG_OFFSET_CAPTURE, + $offset + ); + if (1 == $pregResult) { + $objval = substr($pdfData, $offset, $matches[0][1] - $offset); + $offset = $matches[1][1]; + } + } + } elseif ('endstream' == substr($pdfData, $offset, 9)) { + // end stream object + $objtype = 'endstream'; + $offset += 9; + } elseif (1 == preg_match('/^([0-9]+)[\s]+([0-9]+)[\s]+R/iU', substr($pdfData, $offset, 33), $matches)) { + // indirect object reference + $objtype = 'objref'; + $offset += \strlen($matches[0]); + $objval = (int) $matches[1].'_'.(int) $matches[2]; + } elseif (1 == preg_match('/^([0-9]+)[\s]+([0-9]+)[\s]+obj/iU', substr($pdfData, $offset, 33), $matches)) { + // object start + $objtype = 'obj'; + $objval = (int) $matches[1].'_'.(int) $matches[2]; + $offset += \strlen($matches[0]); + } elseif (($numlen = strspn($pdfData, '+-.0123456789', $offset)) > 0) { + // numeric object + $objtype = 'numeric'; + $objval = substr($pdfData, $offset, $numlen); + $offset += $numlen; + } + break; + } + + return [$objtype, $objval, $offset]; + } + + /** + * Get Cross-Reference (xref) table and trailer data from PDF document data. + * + * @param int $offset xref offset (if known) + * @param array $xref previous xref array (if any) + * + * @return array containing xref and trailer data + * + * @throws \Exception if it was unable to find startxref + * @throws \Exception if it was unable to find xref + */ + protected function getXrefData(string $pdfData, int $offset = 0, array $xref = []): array + { + $startxrefPreg = preg_match( + '/[\r\n]startxref[\s]*[\r\n]+([0-9]+)[\s]*[\r\n]+%%EOF/i', + $pdfData, + $matches, + \PREG_OFFSET_CAPTURE, + $offset + ); + + if (0 == $offset) { + // find last startxref + $pregResult = preg_match_all( + '/[\r\n]startxref[\s]*[\r\n]+([0-9]+)[\s]*[\r\n]+%%EOF/i', + $pdfData, $matches, + \PREG_SET_ORDER, + $offset + ); + if (0 == $pregResult) { + throw new \Exception('Unable to find startxref'); + } + $matches = array_pop($matches); + $startxref = $matches[1]; + } elseif (strpos($pdfData, 'xref', $offset) == $offset) { + // Already pointing at the xref table + $startxref = $offset; + } elseif (preg_match('/([0-9]+[\s][0-9]+[\s]obj)/i', $pdfData, $matches, \PREG_OFFSET_CAPTURE, $offset)) { + // Cross-Reference Stream object + $startxref = $offset; + } elseif ($startxrefPreg) { + // startxref found + $startxref = $matches[1][0]; + } else { + throw new \Exception('Unable to find startxref'); + } + + if ($startxref > \strlen($pdfData)) { + throw new \Exception('Unable to find xref (PDF corrupted?)'); + } + + // check xref position + if (strpos($pdfData, 'xref', $startxref) == $startxref) { + // Cross-Reference + $xref = $this->decodeXref($pdfData, $startxref, $xref); + } else { + // Cross-Reference Stream + $xref = $this->decodeXrefStream($pdfData, $startxref, $xref); + } + if (empty($xref)) { + throw new \Exception('Unable to find xref'); + } + + return $xref; + } + + /** + * Parses PDF data and returns extracted data as array. + * + * @param string $data PDF data to parse + * + * @return array array of parsed PDF document objects + * + * @throws \Exception if empty PDF data given + * @throws \Exception if PDF data missing %PDF header + */ + public function parseData(string $data): array + { + if (empty($data)) { + throw new \Exception('Empty PDF data given.'); + } + // find the pdf header starting position + if (false === ($trimpos = strpos($data, '%PDF-'))) { + throw new \Exception('Invalid PDF data: missing %PDF header.'); + } + + // get PDF content string + $pdfData = $trimpos > 0 ? substr($data, $trimpos) : $data; + + // get xref and trailer data + $xref = $this->getXrefData($pdfData); + + // parse all document objects + $objects = []; + foreach ($xref['xref'] as $obj => $offset) { + if (!isset($objects[$obj]) && ($offset > 0)) { + // decode objects with positive offset + $objects[$obj] = $this->getIndirectObject($pdfData, $xref, $obj, $offset, true); + } + } + + return [$xref, $objects]; + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/XObject/Form.php b/orcinus/pdfparser/src/Smalot/PdfParser/XObject/Form.php new file mode 100644 index 0000000..7caec8c --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/XObject/Form.php @@ -0,0 +1,51 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\XObject; + +use Smalot\PdfParser\Header; +use Smalot\PdfParser\Page; +use Smalot\PdfParser\PDFObject; + +/** + * Class Form + */ +class Form extends Page +{ + public function getText(Page $page = null): string + { + $header = new Header([], $this->document); + $contents = new PDFObject($this->document, $header, $this->content, $this->config); + + return $contents->getText($this); + } +} diff --git a/orcinus/pdfparser/src/Smalot/PdfParser/XObject/Image.php b/orcinus/pdfparser/src/Smalot/PdfParser/XObject/Image.php new file mode 100644 index 0000000..1265582 --- /dev/null +++ b/orcinus/pdfparser/src/Smalot/PdfParser/XObject/Image.php @@ -0,0 +1,47 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. + * If not, see . + */ + +namespace Smalot\PdfParser\XObject; + +use Smalot\PdfParser\Page; +use Smalot\PdfParser\PDFObject; + +/** + * Class Image + */ +class Image extends PDFObject +{ + public function getText(Page $page = null): string + { + return ''; + } +} diff --git a/orcinus/phpmailer/COMMITMENT b/orcinus/phpmailer/COMMITMENT new file mode 100644 index 0000000..a687e0d --- /dev/null +++ b/orcinus/phpmailer/COMMITMENT @@ -0,0 +1,46 @@ +GPL Cooperation Commitment +Version 1.0 + +Before filing or continuing to prosecute any legal proceeding or claim +(other than a Defensive Action) arising from termination of a Covered +License, we commit to extend to the person or entity ('you') accused +of violating the Covered License the following provisions regarding +cure and reinstatement, taken from GPL version 3. As used here, the +term 'this License' refers to the specific Covered License being +enforced. + + However, if you cease all violation of this License, then your + license from a particular copyright holder is reinstated (a) + provisionally, unless and until the copyright holder explicitly + and finally terminates your license, and (b) permanently, if the + copyright holder fails to notify you of the violation by some + reasonable means prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is + reinstated permanently if the copyright holder notifies you of the + violation by some reasonable means, this is the first time you + have received notice of violation of this License (for any work) + from that copyright holder, and you cure the violation prior to 30 + days after your receipt of the notice. + +We intend this Commitment to be irrevocable, and binding and +enforceable against us and assignees of or successors to our +copyrights. + +Definitions + +'Covered License' means the GNU General Public License, version 2 +(GPLv2), the GNU Lesser General Public License, version 2.1 +(LGPLv2.1), or the GNU Library General Public License, version 2 +(LGPLv2), all as published by the Free Software Foundation. + +'Defensive Action' means a legal proceeding or claim that We bring +against you in response to a prior proceeding or claim initiated by +you or your affiliate. + +'We' means each contributor to this repository as of the date of +inclusion of this file, including subsidiaries of a corporate +contributor. + +This work is available under a Creative Commons Attribution-ShareAlike +4.0 International license (https://creativecommons.org/licenses/by-sa/4.0/). diff --git a/orcinus/phpmailer/LICENSE b/orcinus/phpmailer/LICENSE new file mode 100644 index 0000000..f166cc5 --- /dev/null +++ b/orcinus/phpmailer/LICENSE @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! \ No newline at end of file diff --git a/orcinus/phpmailer/README.md b/orcinus/phpmailer/README.md new file mode 100644 index 0000000..53e66f1 --- /dev/null +++ b/orcinus/phpmailer/README.md @@ -0,0 +1,230 @@ +[![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://supportukrainenow.org/) + +![PHPMailer](https://raw.github.com/PHPMailer/PHPMailer/master/examples/images/phpmailer.png) + +# PHPMailer – A full-featured email creation and transfer class for PHP + +[![Test status](https://github.com/PHPMailer/PHPMailer/workflows/Tests/badge.svg)](https://github.com/PHPMailer/PHPMailer/actions) +[![codecov.io](https://codecov.io/gh/PHPMailer/PHPMailer/branch/master/graph/badge.svg?token=iORZpwmYmM)](https://codecov.io/gh/PHPMailer/PHPMailer) +[![Latest Stable Version](https://poser.pugx.org/phpmailer/phpmailer/v/stable.svg)](https://packagist.org/packages/phpmailer/phpmailer) +[![Total Downloads](https://poser.pugx.org/phpmailer/phpmailer/downloads)](https://packagist.org/packages/phpmailer/phpmailer) +[![License](https://poser.pugx.org/phpmailer/phpmailer/license.svg)](https://packagist.org/packages/phpmailer/phpmailer) +[![API Docs](https://github.com/phpmailer/phpmailer/workflows/Docs/badge.svg)](https://phpmailer.github.io/PHPMailer/) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/PHPMailer/PHPMailer/badge)](https://api.securityscorecards.dev/projects/github.com/PHPMailer/PHPMailer) + +## Features +- Probably the world's most popular code for sending email from PHP! +- Used by many open-source projects: WordPress, Drupal, 1CRM, SugarCRM, Yii, Joomla! and many more +- Integrated SMTP support – send without a local mail server +- Send emails with multiple To, CC, BCC, and Reply-to addresses +- Multipart/alternative emails for mail clients that do not read HTML email +- Add attachments, including inline +- Support for UTF-8 content and 8bit, base64, binary, and quoted-printable encodings +- SMTP authentication with LOGIN, PLAIN, CRAM-MD5, and XOAUTH2 mechanisms over SMTPS and SMTP+STARTTLS transports +- Validates email addresses automatically +- Protects against header injection attacks +- Error messages in over 50 languages! +- DKIM and S/MIME signing support +- Compatible with PHP 5.5 and later, including PHP 8.2 +- Namespaced to prevent name clashes +- Much more! + +## Why you might need it +Many PHP developers need to send email from their code. The only PHP function that supports this directly is [`mail()`](https://www.php.net/manual/en/function.mail.php). However, it does not provide any assistance for making use of popular features such as encryption, authentication, HTML messages, and attachments. + +Formatting email correctly is surprisingly difficult. There are myriad overlapping (and conflicting) standards, requiring tight adherence to horribly complicated formatting and encoding rules – the vast majority of code that you'll find online that uses the `mail()` function directly is just plain wrong, if not unsafe! + +The PHP `mail()` function usually sends via a local mail server, typically fronted by a `sendmail` binary on Linux, BSD, and macOS platforms, however, Windows usually doesn't include a local mail server; PHPMailer's integrated SMTP client allows email sending on all platforms without needing a local mail server. Be aware though, that the `mail()` function should be avoided when possible; it's both faster and [safer](https://exploitbox.io/paper/Pwning-PHP-Mail-Function-For-Fun-And-RCE.html) to use SMTP to localhost. + +*Please* don't be tempted to do it yourself – if you don't use PHPMailer, there are many other excellent libraries that +you should look at before rolling your own. Try [SwiftMailer](https://swiftmailer.symfony.com/) +, [Laminas/Mail](https://docs.laminas.dev/laminas-mail/), [ZetaComponents](https://github.com/zetacomponents/Mail), etc. + +## License +This software is distributed under the [LGPL 2.1](http://www.gnu.org/licenses/lgpl-2.1.html) license, along with the [GPL Cooperation Commitment](https://gplcc.github.io/gplcc/). Please read [LICENSE](https://github.com/PHPMailer/PHPMailer/blob/master/LICENSE) for information on the software availability and distribution. + +## Installation & loading +PHPMailer is available on [Packagist](https://packagist.org/packages/phpmailer/phpmailer) (using semantic versioning), and installation via [Composer](https://getcomposer.org) is the recommended way to install PHPMailer. Just add this line to your `composer.json` file: + +```json +"phpmailer/phpmailer": "^6.8.0" +``` + +or run + +```sh +composer require phpmailer/phpmailer +``` + +Note that the `vendor` folder and the `vendor/autoload.php` script are generated by Composer; they are not part of PHPMailer. + +If you want to use the Gmail XOAUTH2 authentication class, you will also need to add a dependency on the `league/oauth2-client` package in your `composer.json`. + +Alternatively, if you're not using Composer, you +can [download PHPMailer as a zip file](https://github.com/PHPMailer/PHPMailer/archive/master.zip), (note that docs and examples are not included in the zip file), then copy the contents of the PHPMailer folder into one of the `include_path` directories specified in your PHP configuration and load each class file manually: + +```php +SMTPDebug = SMTP::DEBUG_SERVER; //Enable verbose debug output + $mail->isSMTP(); //Send using SMTP + $mail->Host = 'smtp.example.com'; //Set the SMTP server to send through + $mail->SMTPAuth = true; //Enable SMTP authentication + $mail->Username = 'user@example.com'; //SMTP username + $mail->Password = 'secret'; //SMTP password + $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; //Enable implicit TLS encryption + $mail->Port = 465; //TCP port to connect to; use 587 if you have set `SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS` + + //Recipients + $mail->setFrom('from@example.com', 'Mailer'); + $mail->addAddress('joe@example.net', 'Joe User'); //Add a recipient + $mail->addAddress('ellen@example.com'); //Name is optional + $mail->addReplyTo('info@example.com', 'Information'); + $mail->addCC('cc@example.com'); + $mail->addBCC('bcc@example.com'); + + //Attachments + $mail->addAttachment('/var/tmp/file.tar.gz'); //Add attachments + $mail->addAttachment('/tmp/image.jpg', 'new.jpg'); //Optional name + + //Content + $mail->isHTML(true); //Set email format to HTML + $mail->Subject = 'Here is the subject'; + $mail->Body = 'This is the HTML message body in bold!'; + $mail->AltBody = 'This is the body in plain text for non-HTML mail clients'; + + $mail->send(); + echo 'Message has been sent'; +} catch (Exception $e) { + echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}"; +} +``` + +You'll find plenty to play with in the [examples](https://github.com/PHPMailer/PHPMailer/tree/master/examples) folder, which covers many common scenarios including sending through Gmail, building contact forms, sending to mailing lists, and more. + +If you are re-using the instance (e.g. when sending to a mailing list), you may need to clear the recipient list to avoid sending duplicate messages. See [the mailing list example](https://github.com/PHPMailer/PHPMailer/blob/master/examples/mailing_list.phps) for further guidance. + +That's it. You should now be ready to use PHPMailer! + +## Localization +PHPMailer defaults to English, but in the [language](https://github.com/PHPMailer/PHPMailer/tree/master/language/) folder, you'll find many translations for PHPMailer error messages that you may encounter. Their filenames contain [ISO 639-1](http://en.wikipedia.org/wiki/ISO_639-1) language code for the translations, for example `fr` for French. To specify a language, you need to tell PHPMailer which one to use, like this: + +```php +//To load the French version +$mail->setLanguage('fr', '/optional/path/to/language/directory/'); +``` + +We welcome corrections and new languages – if you're looking for corrections, run the [PHPMailerLangTest.php](https://github.com/PHPMailer/PHPMailer/tree/master/test/PHPMailerLangTest.php) script in the tests folder and it will show any missing translations. + +## Documentation +Start reading at the [GitHub wiki](https://github.com/PHPMailer/PHPMailer/wiki). If you're having trouble, head for [the troubleshooting guide](https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting) as it's frequently updated. + +Examples of how to use PHPMailer for common scenarios can be found in the [examples](https://github.com/PHPMailer/PHPMailer/tree/master/examples) folder. If you're looking for a good starting point, we recommend you start with [the Gmail example](https://github.com/PHPMailer/PHPMailer/tree/master/examples/gmail.phps). + +To reduce PHPMailer's deployed code footprint, examples are not included if you load PHPMailer via Composer or via [GitHub's zip file download](https://github.com/PHPMailer/PHPMailer/archive/master.zip), so you'll need to either clone the git repository or use the above links to get to the examples directly. + +Complete generated API documentation is [available online](https://phpmailer.github.io/PHPMailer/). + +You can generate complete API-level documentation by running `phpdoc` in the top-level folder, and documentation will appear in the `docs` folder, though you'll need to have [PHPDocumentor](http://www.phpdoc.org) installed. You may find [the unit tests](https://github.com/PHPMailer/PHPMailer/blob/master/test/PHPMailerTest.php) a good reference for how to do various operations such as encryption. + +If the documentation doesn't cover what you need, search the [many questions on Stack Overflow](http://stackoverflow.com/questions/tagged/phpmailer), and before you ask a question about "SMTP Error: Could not connect to SMTP host.", [read the troubleshooting guide](https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting). + +## Tests +[PHPMailer tests](https://github.com/PHPMailer/PHPMailer/tree/master/test/) use PHPUnit 9, with [a polyfill](https://github.com/Yoast/PHPUnit-Polyfills) to let 9-style tests run on older PHPUnit and PHP versions. + +[![Test status](https://github.com/PHPMailer/PHPMailer/workflows/Tests/badge.svg)](https://github.com/PHPMailer/PHPMailer/actions) + +If this isn't passing, is there something you can do to help? + +## Security +Please disclose any vulnerabilities found responsibly – report security issues to the maintainers privately. + +See [SECURITY](https://github.com/PHPMailer/PHPMailer/tree/master/SECURITY.md) and [PHPMailer's security advisories on GitHub](https://github.com/PHPMailer/PHPMailer/security). + +## Contributing +Please submit bug reports, suggestions, and pull requests to the [GitHub issue tracker](https://github.com/PHPMailer/PHPMailer/issues). + +We're particularly interested in fixing edge cases, expanding test coverage, and updating translations. + +If you found a mistake in the docs, or want to add something, go ahead and amend the wiki – anyone can edit it. + +If you have git clones from prior to the move to the PHPMailer GitHub organisation, you'll need to update any remote URLs referencing the old GitHub location with a command like this from within your clone: + +```sh +git remote set-url upstream https://github.com/PHPMailer/PHPMailer.git +``` + +Please *don't* use the SourceForge or Google Code projects any more; they are obsolete and no longer maintained. + +## Sponsorship +Development time and resources for PHPMailer are provided by [Smartmessages.net](https://info.smartmessages.net/), the world's only privacy-first email marketing system. + +Smartmessages.net privacy-first email marketing logo + +Donations are very welcome, whether in beer 🍺, T-shirts 👕, or cold, hard cash 💰. Sponsorship through GitHub is a simple and convenient way to say "thank you" to PHPMailer's maintainers and contributors – just click the "Sponsor" button [on the project page](https://github.com/PHPMailer/PHPMailer). If your company uses PHPMailer, consider taking part in Tidelift's enterprise support programme. + +## PHPMailer For Enterprise + +Available as part of the Tidelift Subscription. + +The maintainers of PHPMailer and thousands of other packages are working with Tidelift to deliver commercial +support and maintenance for the open-source packages you use to build your applications. Save time, reduce risk, and +improve code health, while paying the maintainers of the exact packages you +use. [Learn more.](https://tidelift.com/subscription/pkg/packagist-phpmailer-phpmailer?utm_source=packagist-phpmailer-phpmailer&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) + +## Changelog +See [changelog](changelog.md). + +## History +- PHPMailer was originally written in 2001 by Brent R. Matzelle as a [SourceForge project](http://sourceforge.net/projects/phpmailer/). +- [Marcus Bointon](https://github.com/Synchro) (`coolbru` on SF) and Andy Prevost (`codeworxtech`) took over the project in 2004. +- Became an Apache incubator project on Google Code in 2010, managed by Jim Jagielski. +- Marcus created [his fork on GitHub](https://github.com/Synchro/PHPMailer) in 2008. +- Jim and Marcus decide to join forces and use GitHub as the canonical and official repo for PHPMailer in 2013. +- PHPMailer moves to [the PHPMailer organisation](https://github.com/PHPMailer) on GitHub in 2013. + +### What's changed since moving from SourceForge? +- Official successor to the SourceForge and Google Code projects. +- Test suite. +- Continuous integration with GitHub Actions. +- Composer support. +- Public development. +- Additional languages and language strings. +- CRAM-MD5 authentication support. +- Preserves full repo history of authors, commits, and branches from the original SourceForge project. diff --git a/orcinus/phpmailer/SECURITY.md b/orcinus/phpmailer/SECURITY.md new file mode 100644 index 0000000..035a87f --- /dev/null +++ b/orcinus/phpmailer/SECURITY.md @@ -0,0 +1,37 @@ +# Security notices relating to PHPMailer + +Please disclose any security issues or vulnerabilities found through [Tidelift's coordinated disclosure system](https://tidelift.com/security) or to the maintainers privately. + +PHPMailer 6.4.1 and earlier contain a vulnerability that can result in untrusted code being called (if such code is injected into the host project's scope by other means). If the `$patternselect` parameter to `validateAddress()` is set to `'php'` (the default, defined by `PHPMailer::$validator`), and the global namespace contains a function called `php`, it will be called in preference to the built-in validator of the same name. Mitigated in PHPMailer 6.5.0 by denying the use of simple strings as validator function names. Recorded as [CVE-2021-3603](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-3603). Reported by [Vikrant Singh Chauhan](mailto:vi@hackberry.xyz) via [huntr.dev](https://www.huntr.dev/). + +PHPMailer versions 6.4.1 and earlier contain a possible remote code execution vulnerability through the `$lang_path` parameter of the `setLanguage()` method. If the `$lang_path` parameter is passed unfiltered from user input, it can be set to [a UNC path](https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats#unc-paths), and if an attacker is also able to persuade the server to load a file from that UNC path, a script file under their control may be executed. This vulnerability only applies to systems that resolve UNC paths, typically only Microsoft Windows. +PHPMailer 6.5.0 mitigates this by no longer treating translation files as PHP code, but by parsing their text content directly. This approach avoids the possibility of executing unknown code while retaining backward compatibility. This isn't ideal, so the current translation format is deprecated and will be replaced in the next major release. Recorded as [CVE-2021-34551](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-34551). Reported by [Jilin Diting Information Technology Co., Ltd](https://listensec.com) via Tidelift. + +PHPMailer versions between 6.1.8 and 6.4.0 contain a regression of the earlier CVE-2018-19296 object injection vulnerability as a result of [a fix for Windows UNC paths in 6.1.8](https://github.com/PHPMailer/PHPMailer/commit/e2e07a355ee8ff36aba21d0242c5950c56e4c6f9). Recorded as [CVE-2020-36326](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-36326). Reported by Fariskhi Vidyan via Tidelift. 6.4.1 fixes this issue, and also enforces stricter checks for URL schemes in local path contexts. + +PHPMailer versions 6.1.5 and earlier contain an output escaping bug that occurs in `Content-Type` and `Content-Disposition` when filenames passed into `addAttachment` and other methods that accept attachment names contain double quote characters, in contravention of RFC822 3.4.1. No specific vulnerability has been found relating to this, but it could allow file attachments to bypass attachment filters that are based on matching filename extensions. Recorded as [CVE-2020-13625](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-13625). Reported by Elar Lang of Clarified Security. + +PHPMailer versions prior to 6.0.6 and 5.2.27 are vulnerable to an object injection attack by passing `phar://` paths into `addAttachment()` and other functions that may receive unfiltered local paths, possibly leading to RCE. Recorded as [CVE-2018-19296](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-19296). See [this article](https://knasmueller.net/5-answers-about-php-phar-exploitation) for more info on this type of vulnerability. Mitigated by blocking the use of paths containing URL-protocol style prefixes such as `phar://`. Reported by Sehun Oh of cyberone.kr. + +PHPMailer versions prior to 5.2.24 (released July 26th 2017) have an XSS vulnerability in one of the code examples, [CVE-2017-11503](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2017-11503). The `code_generator.phps` example did not filter user input prior to output. This file is distributed with a `.phps` extension, so it it not normally executable unless it is explicitly renamed, and the file is not included when PHPMailer is loaded through composer, so it is safe by default. There was also an undisclosed potential XSS vulnerability in the default exception handler (unused by default). Patches for both issues kindly provided by Patrick Monnerat of the Fedora Project. + +PHPMailer versions prior to 5.2.22 (released January 9th 2017) have a local file disclosure vulnerability, [CVE-2017-5223](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2017-5223). If content passed into `msgHTML()` is sourced from unfiltered user input, relative paths can map to absolute local file paths and added as attachments. Also note that `addAttachment` (just like `file_get_contents`, `passthru`, `unlink`, etc) should not be passed user-sourced params either! Reported by Yongxiang Li of Asiasecurity. + +PHPMailer versions prior to 5.2.20 (released December 28th 2016) are vulnerable to [CVE-2016-10045](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-10045) a remote code execution vulnerability, responsibly reported by [Dawid Golunski](https://legalhackers.com/advisories/PHPMailer-Exploit-Remote-Code-Exec-CVE-2016-10045-Vuln-Patch-Bypass.html), and patched by Paul Buonopane (@Zenexer). + +PHPMailer versions prior to 5.2.18 (released December 2016) are vulnerable to [CVE-2016-10033](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-10033) a remote code execution vulnerability, responsibly reported by [Dawid Golunski](http://legalhackers.com/advisories/PHPMailer-Exploit-Remote-Code-Exec-CVE-2016-10033-Vuln.html). + +PHPMailer versions prior to 5.2.14 (released November 2015) are vulnerable to [CVE-2015-8476](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-8476) an SMTP CRLF injection bug permitting arbitrary message sending. + +PHPMailer versions prior to 5.2.10 (released May 2015) are vulnerable to [CVE-2008-5619](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2008-5619), a remote code execution vulnerability in the bundled html2text library. This file was removed in 5.2.10, so if you are using a version prior to that and make use of the html2text function, it's vitally important that you upgrade and remove this file. + +PHPMailer versions prior to 2.0.7 and 2.2.1 are vulnerable to [CVE-2012-0796](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2012-0796), an email header injection attack. + +Joomla 1.6.0 uses PHPMailer in an unsafe way, allowing it to reveal local file paths, reported in [CVE-2011-3747](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2011-3747). + +PHPMailer didn't sanitise the `$lang_path` parameter in `SetLanguage`. This wasn't a problem in itself, but some apps (PHPClassifieds, ATutor) also failed to sanitise user-provided parameters passed to it, permitting semi-arbitrary local file inclusion, reported in [CVE-2010-4914](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2010-4914), [CVE-2007-2021](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2007-2021) and [CVE-2006-5734](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2006-5734). + +PHPMailer 1.7.2 and earlier contained a possible DDoS vulnerability reported in [CVE-2005-1807](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2005-1807). + +PHPMailer 1.7 and earlier (June 2003) have a possible vulnerability in the `SendmailSend` method where shell commands may not be sanitised. Reported in [CVE-2007-3215](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2007-3215). + diff --git a/orcinus/phpmailer/VERSION b/orcinus/phpmailer/VERSION new file mode 100644 index 0000000..8a1c5c7 --- /dev/null +++ b/orcinus/phpmailer/VERSION @@ -0,0 +1 @@ +6.8.0 \ No newline at end of file diff --git a/orcinus/phpmailer/composer.json b/orcinus/phpmailer/composer.json new file mode 100644 index 0000000..37e3d6e --- /dev/null +++ b/orcinus/phpmailer/composer.json @@ -0,0 +1,78 @@ +{ + "name": "phpmailer/phpmailer", + "type": "library", + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, + "require": { + "php": ">=5.5.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.2", + "doctrine/annotations": "^1.2.6 || ^1.13.3", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9.3.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.7.1", + "yoast/phpunit-polyfills": "^1.0.4" + }, + "suggest": { + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "ext-openssl": "Needed for secure SMTP sending and DKIM signing", + "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)" + }, + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "PHPMailer\\Test\\": "test/" + } + }, + "license": "LGPL-2.1-only", + "scripts": { + "check": "./vendor/bin/phpcs", + "test": "./vendor/bin/phpunit --no-coverage", + "coverage": "./vendor/bin/phpunit", + "lint": [ + "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . --show-deprecated -e php,phps --exclude vendor --exclude .git --exclude build" + ] + } +} diff --git a/orcinus/phpmailer/get_oauth_token.php b/orcinus/phpmailer/get_oauth_token.php new file mode 100644 index 0000000..cda0445 --- /dev/null +++ b/orcinus/phpmailer/get_oauth_token.php @@ -0,0 +1,182 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2020 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +/** + * Get an OAuth2 token from an OAuth2 provider. + * * Install this script on your server so that it's accessible + * as [https/http]:////get_oauth_token.php + * e.g.: http://localhost/phpmailer/get_oauth_token.php + * * Ensure dependencies are installed with 'composer install' + * * Set up an app in your Google/Yahoo/Microsoft account + * * Set the script address as the app's redirect URL + * If no refresh token is obtained when running this file, + * revoke access to your app and run the script again. + */ + +namespace PHPMailer\PHPMailer; + +/** + * Aliases for League Provider Classes + * Make sure you have added these to your composer.json and run `composer install` + * Plenty to choose from here: + * @see http://oauth2-client.thephpleague.com/providers/thirdparty/ + */ +//@see https://github.com/thephpleague/oauth2-google +use League\OAuth2\Client\Provider\Google; +//@see https://packagist.org/packages/hayageek/oauth2-yahoo +use Hayageek\OAuth2\Client\Provider\Yahoo; +//@see https://github.com/stevenmaguire/oauth2-microsoft +use Stevenmaguire\OAuth2\Client\Provider\Microsoft; +//@see https://github.com/greew/oauth2-azure-provider +use Greew\OAuth2\Client\Provider\Azure; + +if (!isset($_GET['code']) && !isset($_POST['provider'])) { + ?> + + +
+

Select Provider

+ +
+ +
+ +
+ +
+

Enter id and secret

+

These details are obtained by setting up an app in your provider's developer console. +

+

ClientId:

+

ClientSecret:

+

TenantID (only relevant for Azure):

+ +
+ + + $clientId, + 'clientSecret' => $clientSecret, + 'redirectUri' => $redirectUri, + 'accessType' => 'offline' +]; + +$options = []; +$provider = null; + +switch ($providerName) { + case 'Google': + $provider = new Google($params); + $options = [ + 'scope' => [ + 'https://mail.google.com/' + ] + ]; + break; + case 'Yahoo': + $provider = new Yahoo($params); + break; + case 'Microsoft': + $provider = new Microsoft($params); + $options = [ + 'scope' => [ + 'wl.imap', + 'wl.offline_access' + ] + ]; + break; + case 'Azure': + $params['tenantId'] = $tenantId; + + $provider = new Azure($params); + $options = [ + 'scope' => [ + 'https://outlook.office.com/SMTP.Send', + 'offline_access' + ] + ]; + break; +} + +if (null === $provider) { + exit('Provider missing'); +} + +if (!isset($_GET['code'])) { + //If we don't have an authorization code then get one + $authUrl = $provider->getAuthorizationUrl($options); + $_SESSION['oauth2state'] = $provider->getState(); + header('Location: ' . $authUrl); + exit; + //Check given state against previously stored one to mitigate CSRF attack +} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) { + unset($_SESSION['oauth2state']); + unset($_SESSION['provider']); + exit('Invalid state'); +} else { + unset($_SESSION['provider']); + //Try to get an access token (using the authorization code grant) + $token = $provider->getAccessToken( + 'authorization_code', + [ + 'code' => $_GET['code'] + ] + ); + //Use this to interact with an API on the users behalf + //Use this to get a new access token if the old one expires + echo 'Refresh Token: ', $token->getRefreshToken(); +} diff --git a/orcinus/phpmailer/language/phpmailer.lang-af.php b/orcinus/phpmailer/language/phpmailer.lang-af.php new file mode 100644 index 0000000..0b2a72d --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-af.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'خطأ SMTP : لا يمكن تأكيد الهوية.'; +$PHPMAILER_LANG['connect_host'] = 'خطأ SMTP: لا يمكن الاتصال بالخادم SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'خطأ SMTP: لم يتم قبول المعلومات .'; +$PHPMAILER_LANG['empty_message'] = 'نص الرسالة فارغ'; +$PHPMAILER_LANG['encoding'] = 'ترميز غير معروف: '; +$PHPMAILER_LANG['execute'] = 'لا يمكن تنفيذ : '; +$PHPMAILER_LANG['file_access'] = 'لا يمكن الوصول للملف: '; +$PHPMAILER_LANG['file_open'] = 'خطأ في الملف: لا يمكن فتحه: '; +$PHPMAILER_LANG['from_failed'] = 'خطأ على مستوى عنوان المرسل : '; +$PHPMAILER_LANG['instantiate'] = 'لا يمكن توفير خدمة البريد.'; +$PHPMAILER_LANG['invalid_address'] = 'الإرسال غير ممكن لأن عنوان البريد الإلكتروني غير صالح: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' برنامج الإرسال غير مدعوم.'; +$PHPMAILER_LANG['provide_address'] = 'يجب توفير عنوان البريد الإلكتروني لمستلم واحد على الأقل.'; +$PHPMAILER_LANG['recipients_failed'] = 'خطأ SMTP: الأخطاء التالية فشل في الارسال لكل من : '; +$PHPMAILER_LANG['signing'] = 'خطأ في التوقيع: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() غير ممكن.'; +$PHPMAILER_LANG['smtp_error'] = 'خطأ على مستوى الخادم SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'لا يمكن تعيين أو إعادة تعيين متغير: '; +$PHPMAILER_LANG['extension_missing'] = 'الإضافة غير موجودة: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-az.php b/orcinus/phpmailer/language/phpmailer.lang-az.php new file mode 100644 index 0000000..552167e --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-az.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Greška: Neuspjela prijava.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Greška: Nije moguće spojiti se sa SMTP serverom.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Greška: Podatci nisu prihvaćeni.'; +$PHPMAILER_LANG['empty_message'] = 'Sadržaj poruke je prazan.'; +$PHPMAILER_LANG['encoding'] = 'Nepoznata kriptografija: '; +$PHPMAILER_LANG['execute'] = 'Nije moguće izvršiti naredbu: '; +$PHPMAILER_LANG['file_access'] = 'Nije moguće pristupiti datoteci: '; +$PHPMAILER_LANG['file_open'] = 'Nije moguće otvoriti datoteku: '; +$PHPMAILER_LANG['from_failed'] = 'SMTP Greška: Slanje sa navedenih e-mail adresa nije uspjelo: '; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Greška: Slanje na navedene e-mail adrese nije uspjelo: '; +$PHPMAILER_LANG['instantiate'] = 'Ne mogu pokrenuti mail funkcionalnost.'; +$PHPMAILER_LANG['invalid_address'] = 'E-mail nije poslan. Neispravna e-mail adresa: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer nije podržan.'; +$PHPMAILER_LANG['provide_address'] = 'Definišite barem jednu adresu primaoca.'; +$PHPMAILER_LANG['signing'] = 'Greška prilikom prijave: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Spajanje na SMTP server nije uspjelo.'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP greška: '; +$PHPMAILER_LANG['variable_set'] = 'Nije moguće postaviti varijablu ili je vratiti nazad: '; +$PHPMAILER_LANG['extension_missing'] = 'Nedostaje ekstenzija: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-be.php b/orcinus/phpmailer/language/phpmailer.lang-be.php new file mode 100644 index 0000000..9e92dda --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-be.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Памылка SMTP: памылка ідэнтыфікацыі.'; +$PHPMAILER_LANG['connect_host'] = 'Памылка SMTP: нельга ўстанавіць сувязь з SMTP-серверам.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Памылка SMTP: звесткі непрынятыя.'; +$PHPMAILER_LANG['empty_message'] = 'Пустое паведамленне.'; +$PHPMAILER_LANG['encoding'] = 'Невядомая кадыроўка тэксту: '; +$PHPMAILER_LANG['execute'] = 'Нельга выканаць каманду: '; +$PHPMAILER_LANG['file_access'] = 'Няма доступу да файла: '; +$PHPMAILER_LANG['file_open'] = 'Нельга адкрыць файл: '; +$PHPMAILER_LANG['from_failed'] = 'Няправільны адрас адпраўніка: '; +$PHPMAILER_LANG['instantiate'] = 'Нельга прымяніць функцыю mail().'; +$PHPMAILER_LANG['invalid_address'] = 'Нельга даслаць паведамленне, няправільны email атрымальніка: '; +$PHPMAILER_LANG['provide_address'] = 'Запоўніце, калі ласка, правільны email атрымальніка.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' - паштовы сервер не падтрымліваецца.'; +$PHPMAILER_LANG['recipients_failed'] = 'Памылка SMTP: няправільныя атрымальнікі: '; +$PHPMAILER_LANG['signing'] = 'Памылка подпісу паведамлення: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Памылка сувязі з SMTP-серверам.'; +$PHPMAILER_LANG['smtp_error'] = 'Памылка SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Нельга ўстанавіць або перамяніць значэнне пераменнай: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-bg.php b/orcinus/phpmailer/language/phpmailer.lang-bg.php new file mode 100644 index 0000000..c41f675 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-bg.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP грешка: Не може да се удостовери пред сървъра.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP грешка: Не може да се свърже с SMTP хоста.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP грешка: данните не са приети.'; +$PHPMAILER_LANG['empty_message'] = 'Съдържанието на съобщението е празно'; +$PHPMAILER_LANG['encoding'] = 'Неизвестно кодиране: '; +$PHPMAILER_LANG['execute'] = 'Не може да се изпълни: '; +$PHPMAILER_LANG['file_access'] = 'Няма достъп до файл: '; +$PHPMAILER_LANG['file_open'] = 'Файлова грешка: Не може да се отвори файл: '; +$PHPMAILER_LANG['from_failed'] = 'Следните адреси за подател са невалидни: '; +$PHPMAILER_LANG['instantiate'] = 'Не може да се инстанцира функцията mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Невалиден адрес: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' - пощенски сървър не се поддържа.'; +$PHPMAILER_LANG['provide_address'] = 'Трябва да предоставите поне един email адрес за получател.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP грешка: Следните адреси за Получател са невалидни: '; +$PHPMAILER_LANG['signing'] = 'Грешка при подписване: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP провален connect().'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP сървърна грешка: '; +$PHPMAILER_LANG['variable_set'] = 'Не може да се установи или възстанови променлива: '; +$PHPMAILER_LANG['extension_missing'] = 'Липсва разширение: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-ca.php b/orcinus/phpmailer/language/phpmailer.lang-ca.php new file mode 100644 index 0000000..3468485 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-ca.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Error SMTP: No s’ha pogut autenticar.'; +$PHPMAILER_LANG['connect_host'] = 'Error SMTP: No es pot connectar al servidor SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Dades no acceptades.'; +$PHPMAILER_LANG['empty_message'] = 'El cos del missatge està buit.'; +$PHPMAILER_LANG['encoding'] = 'Codificació desconeguda: '; +$PHPMAILER_LANG['execute'] = 'No es pot executar: '; +$PHPMAILER_LANG['file_access'] = 'No es pot accedir a l’arxiu: '; +$PHPMAILER_LANG['file_open'] = 'Error d’Arxiu: No es pot obrir l’arxiu: '; +$PHPMAILER_LANG['from_failed'] = 'La(s) següent(s) adreces de remitent han fallat: '; +$PHPMAILER_LANG['instantiate'] = 'No s’ha pogut crear una instància de la funció Mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Adreça d’email invalida: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer no està suportat'; +$PHPMAILER_LANG['provide_address'] = 'S’ha de proveir almenys una adreça d’email com a destinatari.'; +$PHPMAILER_LANG['recipients_failed'] = 'Error SMTP: Els següents destinataris han fallat: '; +$PHPMAILER_LANG['signing'] = 'Error al signar: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Ha fallat el SMTP Connect().'; +$PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'No s’ha pogut establir o restablir la variable: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-cs.php b/orcinus/phpmailer/language/phpmailer.lang-cs.php new file mode 100644 index 0000000..e770a1a --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-cs.php @@ -0,0 +1,28 @@ + + * Rewrite and extension of the work by Mikael Stokkebro + * + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP fejl: Login mislykkedes.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP fejl: Forbindelse til SMTP serveren kunne ikke oprettes.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP fejl: Data blev ikke accepteret.'; +$PHPMAILER_LANG['empty_message'] = 'Meddelelsen er uden indhold'; +$PHPMAILER_LANG['encoding'] = 'Ukendt encode-format: '; +$PHPMAILER_LANG['execute'] = 'Kunne ikke afvikle: '; +$PHPMAILER_LANG['extension_missing'] = 'Udvidelse mangler: '; +$PHPMAILER_LANG['file_access'] = 'Kunne ikke tilgå filen: '; +$PHPMAILER_LANG['file_open'] = 'Fil fejl: Kunne ikke åbne filen: '; +$PHPMAILER_LANG['from_failed'] = 'Følgende afsenderadresse er forkert: '; +$PHPMAILER_LANG['instantiate'] = 'Email funktionen kunne ikke initialiseres.'; +$PHPMAILER_LANG['invalid_address'] = 'Udgyldig adresse: '; +$PHPMAILER_LANG['invalid_header'] = 'Ugyldig header navn eller værdi'; +$PHPMAILER_LANG['invalid_hostentry'] = 'Ugyldig hostentry: '; +$PHPMAILER_LANG['invalid_host'] = 'Ugyldig vært: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer understøttes ikke.'; +$PHPMAILER_LANG['provide_address'] = 'Indtast mindst en modtagers email adresse.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP fejl: Følgende modtagere fejlede: '; +$PHPMAILER_LANG['signing'] = 'Signeringsfejl: '; +$PHPMAILER_LANG['smtp_code'] = 'SMTP kode: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'Yderligere SMTP info: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() fejlede.'; +$PHPMAILER_LANG['smtp_detail'] = 'Detalje: '; +$PHPMAILER_LANG['smtp_error'] = 'SMTP server fejl: '; +$PHPMAILER_LANG['variable_set'] = 'Kunne ikke definere eller nulstille variablen: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-de.php b/orcinus/phpmailer/language/phpmailer.lang-de.php new file mode 100644 index 0000000..e7e59d2 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-de.php @@ -0,0 +1,28 @@ + + * @author Crystopher Glodzienski Cardoso + */ + +$PHPMAILER_LANG['authenticate'] = 'Error SMTP: Imposible autentificar.'; +$PHPMAILER_LANG['connect_host'] = 'Error SMTP: Imposible conectar al servidor SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Datos no aceptados.'; +$PHPMAILER_LANG['empty_message'] = 'El cuerpo del mensaje está vacío.'; +$PHPMAILER_LANG['encoding'] = 'Codificación desconocida: '; +$PHPMAILER_LANG['execute'] = 'Imposible ejecutar: '; +$PHPMAILER_LANG['file_access'] = 'Imposible acceder al archivo: '; +$PHPMAILER_LANG['file_open'] = 'Error de Archivo: Imposible abrir el archivo: '; +$PHPMAILER_LANG['from_failed'] = 'La(s) siguiente(s) direcciones de remitente fallaron: '; +$PHPMAILER_LANG['instantiate'] = 'Imposible crear una instancia de la función Mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Imposible enviar: dirección de email inválido: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer no está soportado.'; +$PHPMAILER_LANG['provide_address'] = 'Debe proporcionar al menos una dirección de email de destino.'; +$PHPMAILER_LANG['recipients_failed'] = 'Error SMTP: Los siguientes destinos fallaron: '; +$PHPMAILER_LANG['signing'] = 'Error al firmar: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falló.'; +$PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'No se pudo configurar la variable: '; +$PHPMAILER_LANG['extension_missing'] = 'Extensión faltante: '; +$PHPMAILER_LANG['smtp_code'] = 'Código del servidor SMTP: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'Información adicional del servidor SMTP: '; +$PHPMAILER_LANG['invalid_header'] = 'Nombre o valor de encabezado no válido'; diff --git a/orcinus/phpmailer/language/phpmailer.lang-et.php b/orcinus/phpmailer/language/phpmailer.lang-et.php new file mode 100644 index 0000000..93addc9 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-et.php @@ -0,0 +1,28 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Viga: Autoriseerimise viga.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Viga: Ei õnnestunud luua ühendust SMTP serveriga.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Viga: Vigased andmed.'; +$PHPMAILER_LANG['empty_message'] = 'Tühi kirja sisu'; +$PHPMAILER_LANG["encoding"] = 'Tundmatu kodeering: '; +$PHPMAILER_LANG['execute'] = 'Tegevus ebaõnnestus: '; +$PHPMAILER_LANG['file_access'] = 'Pole piisavalt õiguseid järgneva faili avamiseks: '; +$PHPMAILER_LANG['file_open'] = 'Faili Viga: Faili avamine ebaõnnestus: '; +$PHPMAILER_LANG['from_failed'] = 'Järgnev saatja e-posti aadress on vigane: '; +$PHPMAILER_LANG['instantiate'] = 'mail funktiooni käivitamine ebaõnnestus.'; +$PHPMAILER_LANG['invalid_address'] = 'Saatmine peatatud, e-posti address vigane: '; +$PHPMAILER_LANG['provide_address'] = 'Te peate määrama vähemalt ühe saaja e-posti aadressi.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' maileri tugi puudub.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Viga: Järgnevate saajate e-posti aadressid on vigased: '; +$PHPMAILER_LANG["signing"] = 'Viga allkirjastamisel: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() ebaõnnestus.'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP serveri viga: '; +$PHPMAILER_LANG['variable_set'] = 'Ei õnnestunud määrata või lähtestada muutujat: '; +$PHPMAILER_LANG['extension_missing'] = 'Nõutud laiendus on puudu: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-fa.php b/orcinus/phpmailer/language/phpmailer.lang-fa.php new file mode 100644 index 0000000..295a47f --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-fa.php @@ -0,0 +1,28 @@ + + * @author Mohammad Hossein Mojtahedi + */ + +$PHPMAILER_LANG['authenticate'] = 'خطای SMTP: احراز هویت با شکست مواجه شد.'; +$PHPMAILER_LANG['connect_host'] = 'خطای SMTP: اتصال به سرور SMTP برقرار نشد.'; +$PHPMAILER_LANG['data_not_accepted'] = 'خطای SMTP: داده‌ها نا‌درست هستند.'; +$PHPMAILER_LANG['empty_message'] = 'بخش متن پیام خالی است.'; +$PHPMAILER_LANG['encoding'] = 'کد‌گذاری نا‌شناخته: '; +$PHPMAILER_LANG['execute'] = 'امکان اجرا وجود ندارد: '; +$PHPMAILER_LANG['file_access'] = 'امکان دسترسی به فایل وجود ندارد: '; +$PHPMAILER_LANG['file_open'] = 'خطای File: امکان بازکردن فایل وجود ندارد: '; +$PHPMAILER_LANG['from_failed'] = 'آدرس فرستنده اشتباه است: '; +$PHPMAILER_LANG['instantiate'] = 'امکان معرفی تابع ایمیل وجود ندارد.'; +$PHPMAILER_LANG['invalid_address'] = 'آدرس ایمیل معتبر نیست: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer پشتیبانی نمی‌شود.'; +$PHPMAILER_LANG['provide_address'] = 'باید حداقل یک آدرس گیرنده وارد کنید.'; +$PHPMAILER_LANG['recipients_failed'] = 'خطای SMTP: ارسال به آدرس گیرنده با خطا مواجه شد: '; +$PHPMAILER_LANG['signing'] = 'خطا در امضا: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'خطا در اتصال به SMTP.'; +$PHPMAILER_LANG['smtp_error'] = 'خطا در SMTP Server: '; +$PHPMAILER_LANG['variable_set'] = 'امکان ارسال یا ارسال مجدد متغیر‌ها وجود ندارد: '; +$PHPMAILER_LANG['extension_missing'] = 'افزونه موجود نیست: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-fi.php b/orcinus/phpmailer/language/phpmailer.lang-fi.php new file mode 100644 index 0000000..243c054 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-fi.php @@ -0,0 +1,28 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP feilur: Kundi ikki góðkenna.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP feilur: Kundi ikki knýta samband við SMTP vert.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP feilur: Data ikki góðkent.'; +//$PHPMAILER_LANG['empty_message'] = 'Message body empty'; +$PHPMAILER_LANG['encoding'] = 'Ókend encoding: '; +$PHPMAILER_LANG['execute'] = 'Kundi ikki útføra: '; +$PHPMAILER_LANG['file_access'] = 'Kundi ikki tilganga fílu: '; +$PHPMAILER_LANG['file_open'] = 'Fílu feilur: Kundi ikki opna fílu: '; +$PHPMAILER_LANG['from_failed'] = 'fylgjandi Frá/From adressa miseydnaðist: '; +$PHPMAILER_LANG['instantiate'] = 'Kuni ikki instantiera mail funktión.'; +//$PHPMAILER_LANG['invalid_address'] = 'Invalid address: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' er ikki supporterað.'; +$PHPMAILER_LANG['provide_address'] = 'Tú skal uppgeva minst móttakara-emailadressu(r).'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Feilur: Fylgjandi móttakarar miseydnaðust: '; +//$PHPMAILER_LANG['signing'] = 'Signing Error: '; +//$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.'; +//$PHPMAILER_LANG['smtp_error'] = 'SMTP server error: '; +//$PHPMAILER_LANG['variable_set'] = 'Cannot set or reset variable: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-fr.php b/orcinus/phpmailer/language/phpmailer.lang-fr.php new file mode 100644 index 0000000..38a7a8e --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-fr.php @@ -0,0 +1,38 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Erro SMTP: Non puido ser autentificado.'; +$PHPMAILER_LANG['connect_host'] = 'Erro SMTP: Non puido conectar co servidor SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Erro SMTP: Datos non aceptados.'; +$PHPMAILER_LANG['empty_message'] = 'Corpo da mensaxe vacía'; +$PHPMAILER_LANG['encoding'] = 'Codificación descoñecida: '; +$PHPMAILER_LANG['execute'] = 'Non puido ser executado: '; +$PHPMAILER_LANG['file_access'] = 'Nob puido acceder ó arquivo: '; +$PHPMAILER_LANG['file_open'] = 'Erro de Arquivo: No puido abrir o arquivo: '; +$PHPMAILER_LANG['from_failed'] = 'A(s) seguinte(s) dirección(s) de remitente(s) deron erro: '; +$PHPMAILER_LANG['instantiate'] = 'Non puido crear unha instancia da función Mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Non puido envia-lo correo: dirección de email inválida: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer non está soportado.'; +$PHPMAILER_LANG['provide_address'] = 'Debe engadir polo menos unha dirección de email coma destino.'; +$PHPMAILER_LANG['recipients_failed'] = 'Erro SMTP: Os seguintes destinos fallaron: '; +$PHPMAILER_LANG['signing'] = 'Erro ó firmar: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() fallou.'; +$PHPMAILER_LANG['smtp_error'] = 'Erro do servidor SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Non puidemos axustar ou reaxustar a variábel: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-he.php b/orcinus/phpmailer/language/phpmailer.lang-he.php new file mode 100644 index 0000000..b123aa5 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-he.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'שגיאת SMTP: פעולת האימות נכשלה.'; +$PHPMAILER_LANG['connect_host'] = 'שגיאת SMTP: לא הצלחתי להתחבר לשרת SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'שגיאת SMTP: מידע לא התקבל.'; +$PHPMAILER_LANG['empty_message'] = 'גוף ההודעה ריק'; +$PHPMAILER_LANG['invalid_address'] = 'כתובת שגויה: '; +$PHPMAILER_LANG['encoding'] = 'קידוד לא מוכר: '; +$PHPMAILER_LANG['execute'] = 'לא הצלחתי להפעיל את: '; +$PHPMAILER_LANG['file_access'] = 'לא ניתן לגשת לקובץ: '; +$PHPMAILER_LANG['file_open'] = 'שגיאת קובץ: לא ניתן לגשת לקובץ: '; +$PHPMAILER_LANG['from_failed'] = 'כתובות הנמענים הבאות נכשלו: '; +$PHPMAILER_LANG['instantiate'] = 'לא הצלחתי להפעיל את פונקציית המייל.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' אינה נתמכת.'; +$PHPMAILER_LANG['provide_address'] = 'חובה לספק לפחות כתובת אחת של מקבל המייל.'; +$PHPMAILER_LANG['recipients_failed'] = 'שגיאת SMTP: הנמענים הבאים נכשלו: '; +$PHPMAILER_LANG['signing'] = 'שגיאת חתימה: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.'; +$PHPMAILER_LANG['smtp_error'] = 'שגיאת שרת SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'לא ניתן לקבוע או לשנות את המשתנה: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-hi.php b/orcinus/phpmailer/language/phpmailer.lang-hi.php new file mode 100644 index 0000000..d2856e0 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-hi.php @@ -0,0 +1,35 @@ + + * Rewrite and extension of the work by Jayanti Suthar + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP त्रुटि: प्रामाणिकता की जांच नहीं हो सका। '; +$PHPMAILER_LANG['buggy_php'] = 'PHP का आपका संस्करण एक बग से प्रभावित है जिसके परिणामस्वरूप संदेश दूषित हो सकते हैं. इसे ठीक करने हेतु, भेजने के लिए SMTP का उपयोग करे, अपने php.ini में mail.add_x_header विकल्प को अक्षम करें, MacOS या Linux पर जाए, या अपने PHP संस्करण को 7.0.17+ या 7.1.3+ बदले.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP त्रुटि: SMTP सर्वर से कनेक्ट नहीं हो सका। '; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP त्रुटि: डेटा स्वीकार नहीं किया जाता है। '; +$PHPMAILER_LANG['empty_message'] = 'संदेश खाली है। '; +$PHPMAILER_LANG['encoding'] = 'अज्ञात एन्कोडिंग प्रकार। '; +$PHPMAILER_LANG['execute'] = 'आदेश को निष्पादित करने में विफल। '; +$PHPMAILER_LANG['extension_missing'] = 'एक्सटेन्षन गायब है: '; +$PHPMAILER_LANG['file_access'] = 'फ़ाइल उपलब्ध नहीं है। '; +$PHPMAILER_LANG['file_open'] = 'फ़ाइल त्रुटि: फाइल को खोला नहीं जा सका। '; +$PHPMAILER_LANG['from_failed'] = 'प्रेषक का पता गलत है। '; +$PHPMAILER_LANG['instantiate'] = 'मेल फ़ंक्शन कॉल नहीं कर सकता है।'; +$PHPMAILER_LANG['invalid_address'] = 'पता गलत है। '; +$PHPMAILER_LANG['invalid_header'] = 'अमान्य हेडर नाम या मान'; +$PHPMAILER_LANG['invalid_hostentry'] = 'अमान्य hostentry: '; +$PHPMAILER_LANG['invalid_host'] = 'अमान्य होस्ट: '; +$PHPMAILER_LANG['mailer_not_supported'] = 'मेल सर्वर के साथ काम नहीं करता है। '; +$PHPMAILER_LANG['provide_address'] = 'आपको कम से कम एक प्राप्तकर्ता का ई-मेल पता प्रदान करना होगा।'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP त्रुटि: निम्न प्राप्तकर्ताओं को पते भेजने में विफल। '; +$PHPMAILER_LANG['signing'] = 'साइनअप त्रुटि: '; +$PHPMAILER_LANG['smtp_code'] = 'SMTP कोड: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'अतिरिक्त SMTP जानकारी: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP का connect () फ़ंक्शन विफल हुआ। '; +$PHPMAILER_LANG['smtp_detail'] = 'विवरण: '; +$PHPMAILER_LANG['smtp_error'] = 'SMTP सर्वर त्रुटि। '; +$PHPMAILER_LANG['variable_set'] = 'चर को बना या संशोधित नहीं किया जा सकता। '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-hr.php b/orcinus/phpmailer/language/phpmailer.lang-hr.php new file mode 100644 index 0000000..cacb6c3 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-hr.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Greška: Neuspjela autentikacija.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Greška: Ne mogu se spojiti na SMTP poslužitelj.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Greška: Podatci nisu prihvaćeni.'; +$PHPMAILER_LANG['empty_message'] = 'Sadržaj poruke je prazan.'; +$PHPMAILER_LANG['encoding'] = 'Nepoznati encoding: '; +$PHPMAILER_LANG['execute'] = 'Nije moguće izvršiti naredbu: '; +$PHPMAILER_LANG['file_access'] = 'Nije moguće pristupiti datoteci: '; +$PHPMAILER_LANG['file_open'] = 'Nije moguće otvoriti datoteku: '; +$PHPMAILER_LANG['from_failed'] = 'SMTP Greška: Slanje s navedenih e-mail adresa nije uspjelo: '; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Greška: Slanje na navedenih e-mail adresa nije uspjelo: '; +$PHPMAILER_LANG['instantiate'] = 'Ne mogu pokrenuti mail funkcionalnost.'; +$PHPMAILER_LANG['invalid_address'] = 'E-mail nije poslan. Neispravna e-mail adresa: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer nije podržan.'; +$PHPMAILER_LANG['provide_address'] = 'Definirajte barem jednu adresu primatelja.'; +$PHPMAILER_LANG['signing'] = 'Greška prilikom prijave: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Spajanje na SMTP poslužitelj nije uspjelo.'; +$PHPMAILER_LANG['smtp_error'] = 'Greška SMTP poslužitelja: '; +$PHPMAILER_LANG['variable_set'] = 'Ne mogu postaviti varijablu niti ju vratiti nazad: '; +$PHPMAILER_LANG['extension_missing'] = 'Nedostaje proširenje: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-hu.php b/orcinus/phpmailer/language/phpmailer.lang-hu.php new file mode 100644 index 0000000..e6b58b0 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-hu.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP -ի սխալ: չհաջողվեց ստուգել իսկությունը.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP -ի սխալ: չհաջողվեց կապ հաստատել SMTP սերվերի հետ.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP -ի սխալ: տվյալները ընդունված չեն.'; +$PHPMAILER_LANG['empty_message'] = 'Հաղորդագրությունը դատարկ է'; +$PHPMAILER_LANG['encoding'] = 'Կոդավորման անհայտ տեսակ: '; +$PHPMAILER_LANG['execute'] = 'Չհաջողվեց իրականացնել հրամանը: '; +$PHPMAILER_LANG['file_access'] = 'Ֆայլը հասանելի չէ: '; +$PHPMAILER_LANG['file_open'] = 'Ֆայլի սխալ: ֆայլը չհաջողվեց բացել: '; +$PHPMAILER_LANG['from_failed'] = 'Ուղարկողի հետևյալ հասցեն սխալ է: '; +$PHPMAILER_LANG['instantiate'] = 'Հնարավոր չէ կանչել mail ֆունկցիան.'; +$PHPMAILER_LANG['invalid_address'] = 'Հասցեն սխալ է: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' փոստային սերվերի հետ չի աշխատում.'; +$PHPMAILER_LANG['provide_address'] = 'Անհրաժեշտ է տրամադրել գոնե մեկ ստացողի e-mail հասցե.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP -ի սխալ: չի հաջողվել ուղարկել հետևյալ ստացողների հասցեներին: '; +$PHPMAILER_LANG['signing'] = 'Ստորագրման սխալ: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP -ի connect() ֆունկցիան չի հաջողվել'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP սերվերի սխալ: '; +$PHPMAILER_LANG['variable_set'] = 'Չի հաջողվում ստեղծել կամ վերափոխել փոփոխականը: '; +$PHPMAILER_LANG['extension_missing'] = 'Հավելվածը բացակայում է: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-id.php b/orcinus/phpmailer/language/phpmailer.lang-id.php new file mode 100644 index 0000000..212a11f --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-id.php @@ -0,0 +1,31 @@ + + * @author @januridp + * @author Ian Mustafa + */ + +$PHPMAILER_LANG['authenticate'] = 'Kesalahan SMTP: Tidak dapat mengotentikasi.'; +$PHPMAILER_LANG['connect_host'] = 'Kesalahan SMTP: Tidak dapat terhubung ke host SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Kesalahan SMTP: Data tidak diterima.'; +$PHPMAILER_LANG['empty_message'] = 'Isi pesan kosong'; +$PHPMAILER_LANG['encoding'] = 'Pengkodean karakter tidak dikenali: '; +$PHPMAILER_LANG['execute'] = 'Tidak dapat menjalankan proses: '; +$PHPMAILER_LANG['file_access'] = 'Tidak dapat mengakses berkas: '; +$PHPMAILER_LANG['file_open'] = 'Kesalahan Berkas: Berkas tidak dapat dibuka: '; +$PHPMAILER_LANG['from_failed'] = 'Alamat pengirim berikut mengakibatkan kesalahan: '; +$PHPMAILER_LANG['instantiate'] = 'Tidak dapat menginisialisasi fungsi surel.'; +$PHPMAILER_LANG['invalid_address'] = 'Gagal terkirim, alamat surel tidak sesuai: '; +$PHPMAILER_LANG['invalid_hostentry'] = 'Gagal terkirim, entri host tidak sesuai: '; +$PHPMAILER_LANG['invalid_host'] = 'Gagal terkirim, host tidak sesuai: '; +$PHPMAILER_LANG['provide_address'] = 'Harus tersedia minimal satu alamat tujuan'; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer tidak didukung'; +$PHPMAILER_LANG['recipients_failed'] = 'Kesalahan SMTP: Alamat tujuan berikut menyebabkan kesalahan: '; +$PHPMAILER_LANG['signing'] = 'Kesalahan dalam penandatangan SSL: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() gagal.'; +$PHPMAILER_LANG['smtp_error'] = 'Kesalahan pada pelayan SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Tidak dapat mengatur atau mengatur ulang variabel: '; +$PHPMAILER_LANG['extension_missing'] = 'Ekstensi PHP tidak tersedia: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-it.php b/orcinus/phpmailer/language/phpmailer.lang-it.php new file mode 100644 index 0000000..08a6b73 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-it.php @@ -0,0 +1,28 @@ + + * @author Stefano Sabatini + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Error: Impossibile autenticarsi.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Impossibile connettersi all\'host SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Error: Dati non accettati dal server.'; +$PHPMAILER_LANG['empty_message'] = 'Il corpo del messaggio è vuoto'; +$PHPMAILER_LANG['encoding'] = 'Codifica dei caratteri sconosciuta: '; +$PHPMAILER_LANG['execute'] = 'Impossibile eseguire l\'operazione: '; +$PHPMAILER_LANG['file_access'] = 'Impossibile accedere al file: '; +$PHPMAILER_LANG['file_open'] = 'File Error: Impossibile aprire il file: '; +$PHPMAILER_LANG['from_failed'] = 'I seguenti indirizzi mittenti hanno generato errore: '; +$PHPMAILER_LANG['instantiate'] = 'Impossibile istanziare la funzione mail'; +$PHPMAILER_LANG['invalid_address'] = 'Impossibile inviare, l\'indirizzo email non è valido: '; +$PHPMAILER_LANG['provide_address'] = 'Deve essere fornito almeno un indirizzo ricevente'; +$PHPMAILER_LANG['mailer_not_supported'] = 'Mailer non supportato'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: I seguenti indirizzi destinatari hanno generato un errore: '; +$PHPMAILER_LANG['signing'] = 'Errore nella firma: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() fallita.'; +$PHPMAILER_LANG['smtp_error'] = 'Errore del server SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Impossibile impostare o resettare la variabile: '; +$PHPMAILER_LANG['extension_missing'] = 'Estensione mancante: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-ja.php b/orcinus/phpmailer/language/phpmailer.lang-ja.php new file mode 100644 index 0000000..c76f526 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-ja.php @@ -0,0 +1,29 @@ + + * @author Yoshi Sakai + * @author Arisophy + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTPエラー: 認証できませんでした。'; +$PHPMAILER_LANG['connect_host'] = 'SMTPエラー: SMTPホストに接続できませんでした。'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTPエラー: データが受け付けられませんでした。'; +$PHPMAILER_LANG['empty_message'] = 'メール本文が空です。'; +$PHPMAILER_LANG['encoding'] = '不明なエンコーディング: '; +$PHPMAILER_LANG['execute'] = '実行できませんでした: '; +$PHPMAILER_LANG['file_access'] = 'ファイルにアクセスできません: '; +$PHPMAILER_LANG['file_open'] = 'ファイルエラー: ファイルを開けません: '; +$PHPMAILER_LANG['from_failed'] = 'Fromアドレスを登録する際にエラーが発生しました: '; +$PHPMAILER_LANG['instantiate'] = 'メール関数が正常に動作しませんでした。'; +$PHPMAILER_LANG['invalid_address'] = '不正なメールアドレス: '; +$PHPMAILER_LANG['provide_address'] = '少なくとも1つメールアドレスを 指定する必要があります。'; +$PHPMAILER_LANG['mailer_not_supported'] = ' メーラーがサポートされていません。'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTPエラー: 次の受信者アドレスに 間違いがあります: '; +$PHPMAILER_LANG['signing'] = '署名エラー: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP接続に失敗しました。'; +$PHPMAILER_LANG['smtp_error'] = 'SMTPサーバーエラー: '; +$PHPMAILER_LANG['variable_set'] = '変数が存在しません: '; +$PHPMAILER_LANG['extension_missing'] = '拡張機能が見つかりません: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-ka.php b/orcinus/phpmailer/language/phpmailer.lang-ka.php new file mode 100644 index 0000000..51fe403 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-ka.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP შეცდომა: ავტორიზაცია შეუძლებელია.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP შეცდომა: SMTP სერვერთან დაკავშირება შეუძლებელია.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP შეცდომა: მონაცემები არ იქნა მიღებული.'; +$PHPMAILER_LANG['encoding'] = 'კოდირების უცნობი ტიპი: '; +$PHPMAILER_LANG['execute'] = 'შეუძლებელია შემდეგი ბრძანების შესრულება: '; +$PHPMAILER_LANG['file_access'] = 'შეუძლებელია წვდომა ფაილთან: '; +$PHPMAILER_LANG['file_open'] = 'ფაილური სისტემის შეცდომა: არ იხსნება ფაილი: '; +$PHPMAILER_LANG['from_failed'] = 'გამგზავნის არასწორი მისამართი: '; +$PHPMAILER_LANG['instantiate'] = 'mail ფუნქციის გაშვება ვერ ხერხდება.'; +$PHPMAILER_LANG['provide_address'] = 'გთხოვთ მიუთითოთ ერთი ადრესატის e-mail მისამართი მაინც.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' - საფოსტო სერვერის მხარდაჭერა არ არის.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP შეცდომა: შემდეგ მისამართებზე გაგზავნა ვერ მოხერხდა: '; +$PHPMAILER_LANG['empty_message'] = 'შეტყობინება ცარიელია'; +$PHPMAILER_LANG['invalid_address'] = 'არ გაიგზავნა, e-mail მისამართის არასწორი ფორმატი: '; +$PHPMAILER_LANG['signing'] = 'ხელმოწერის შეცდომა: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'შეცდომა SMTP სერვერთან დაკავშირებისას'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP სერვერის შეცდომა: '; +$PHPMAILER_LANG['variable_set'] = 'შეუძლებელია შემდეგი ცვლადის შექმნა ან შეცვლა: '; +$PHPMAILER_LANG['extension_missing'] = 'ბიბლიოთეკა არ არსებობს: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-ko.php b/orcinus/phpmailer/language/phpmailer.lang-ko.php new file mode 100644 index 0000000..8c97dd9 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-ko.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP 오류: 인증할 수 없습니다.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP 오류: SMTP 호스트에 접속할 수 없습니다.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP 오류: 데이터가 받아들여지지 않았습니다.'; +$PHPMAILER_LANG['empty_message'] = '메세지 내용이 없습니다'; +$PHPMAILER_LANG['encoding'] = '알 수 없는 인코딩: '; +$PHPMAILER_LANG['execute'] = '실행 불가: '; +$PHPMAILER_LANG['file_access'] = '파일 접근 불가: '; +$PHPMAILER_LANG['file_open'] = '파일 오류: 파일을 열 수 없습니다: '; +$PHPMAILER_LANG['from_failed'] = '다음 From 주소에서 오류가 발생했습니다: '; +$PHPMAILER_LANG['instantiate'] = 'mail 함수를 인스턴스화할 수 없습니다'; +$PHPMAILER_LANG['invalid_address'] = '잘못된 주소: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' 메일러는 지원되지 않습니다.'; +$PHPMAILER_LANG['provide_address'] = '적어도 한 개 이상의 수신자 메일 주소를 제공해야 합니다.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP 오류: 다음 수신자에서 오류가 발생했습니다: '; +$PHPMAILER_LANG['signing'] = '서명 오류: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP 연결을 실패하였습니다.'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP 서버 오류: '; +$PHPMAILER_LANG['variable_set'] = '변수 설정 및 초기화 불가: '; +$PHPMAILER_LANG['extension_missing'] = '확장자 없음: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-lt.php b/orcinus/phpmailer/language/phpmailer.lang-lt.php new file mode 100644 index 0000000..4f115b1 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-lt.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP klaida: autentifikacija nepavyko.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP klaida: nepavyksta prisijungti prie SMTP stoties.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP klaida: duomenys nepriimti.'; +$PHPMAILER_LANG['empty_message'] = 'Laiško turinys tuščias'; +$PHPMAILER_LANG['encoding'] = 'Neatpažinta koduotė: '; +$PHPMAILER_LANG['execute'] = 'Nepavyko įvykdyti komandos: '; +$PHPMAILER_LANG['file_access'] = 'Byla nepasiekiama: '; +$PHPMAILER_LANG['file_open'] = 'Bylos klaida: Nepavyksta atidaryti: '; +$PHPMAILER_LANG['from_failed'] = 'Neteisingas siuntėjo adresas: '; +$PHPMAILER_LANG['instantiate'] = 'Nepavyko paleisti mail funkcijos.'; +$PHPMAILER_LANG['invalid_address'] = 'Neteisingas adresas: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' pašto stotis nepalaikoma.'; +$PHPMAILER_LANG['provide_address'] = 'Nurodykite bent vieną gavėjo adresą.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP klaida: nepavyko išsiųsti šiems gavėjams: '; +$PHPMAILER_LANG['signing'] = 'Prisijungimo klaida: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP susijungimo klaida'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP stoties klaida: '; +$PHPMAILER_LANG['variable_set'] = 'Nepavyko priskirti reikšmės kintamajam: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-lv.php b/orcinus/phpmailer/language/phpmailer.lang-lv.php new file mode 100644 index 0000000..679b18c --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-lv.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP kļūda: Autorizācija neizdevās.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Kļūda: Nevar izveidot savienojumu ar SMTP serveri.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Kļūda: Nepieņem informāciju.'; +$PHPMAILER_LANG['empty_message'] = 'Ziņojuma teksts ir tukšs'; +$PHPMAILER_LANG['encoding'] = 'Neatpazīts kodējums: '; +$PHPMAILER_LANG['execute'] = 'Neizdevās izpildīt komandu: '; +$PHPMAILER_LANG['file_access'] = 'Fails nav pieejams: '; +$PHPMAILER_LANG['file_open'] = 'Faila kļūda: Nevar atvērt failu: '; +$PHPMAILER_LANG['from_failed'] = 'Nepareiza sūtītāja adrese: '; +$PHPMAILER_LANG['instantiate'] = 'Nevar palaist sūtīšanas funkciju.'; +$PHPMAILER_LANG['invalid_address'] = 'Nepareiza adrese: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' sūtītājs netiek atbalstīts.'; +$PHPMAILER_LANG['provide_address'] = 'Lūdzu, norādiet vismaz vienu adresātu.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP kļūda: neizdevās nosūtīt šādiem saņēmējiem: '; +$PHPMAILER_LANG['signing'] = 'Autorizācijas kļūda: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP savienojuma kļūda'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP servera kļūda: '; +$PHPMAILER_LANG['variable_set'] = 'Nevar piešķirt mainīgā vērtību: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-mg.php b/orcinus/phpmailer/language/phpmailer.lang-mg.php new file mode 100644 index 0000000..8a94f6a --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-mg.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Hadisoana SMTP: Tsy nahomby ny fanamarinana.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Tsy afaka mampifandray amin\'ny mpampiantrano SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP diso: tsy voarakitra ny angona.'; +$PHPMAILER_LANG['empty_message'] = 'Tsy misy ny votoaty mailaka.'; +$PHPMAILER_LANG['encoding'] = 'Tsy fantatra encoding: '; +$PHPMAILER_LANG['execute'] = 'Tsy afaka manatanteraka ity baiko manaraka ity: '; +$PHPMAILER_LANG['file_access'] = 'Tsy nahomby ny fidirana amin\'ity rakitra ity: '; +$PHPMAILER_LANG['file_open'] = 'Hadisoana diso: Tsy afaka nanokatra ity file manaraka ity: '; +$PHPMAILER_LANG['from_failed'] = 'Ny adiresy iraka manaraka dia diso: '; +$PHPMAILER_LANG['instantiate'] = 'Tsy afaka nanomboka ny hetsika mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Tsy mety ny adiresy: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer tsy manohana.'; +$PHPMAILER_LANG['provide_address'] = 'Alefaso azafady iray adiresy iray farafahakeliny.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: Tsy mety ireo mpanaraka ireto: '; +$PHPMAILER_LANG['signing'] = 'Error nandritra ny sonia:'; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Tsy nahomby ny fifandraisana tamin\'ny server SMTP.'; +$PHPMAILER_LANG['smtp_error'] = 'Fahadisoana tamin\'ny server SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Tsy azo atao ny mametraka na mamerina ny variable: '; +$PHPMAILER_LANG['extension_missing'] = 'Tsy hita ny ampahany: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-mn.php b/orcinus/phpmailer/language/phpmailer.lang-mn.php new file mode 100644 index 0000000..04d262c --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-mn.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Ralat SMTP: Tidak dapat pengesahan.'; +$PHPMAILER_LANG['connect_host'] = 'Ralat SMTP: Tidak dapat menghubungi hos pelayan SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Ralat SMTP: Data tidak diterima oleh pelayan.'; +$PHPMAILER_LANG['empty_message'] = 'Tiada isi untuk mesej'; +$PHPMAILER_LANG['encoding'] = 'Pengekodan tidak diketahui: '; +$PHPMAILER_LANG['execute'] = 'Tidak dapat melaksanakan: '; +$PHPMAILER_LANG['file_access'] = 'Tidak dapat mengakses fail: '; +$PHPMAILER_LANG['file_open'] = 'Ralat Fail: Tidak dapat membuka fail: '; +$PHPMAILER_LANG['from_failed'] = 'Berikut merupakan ralat dari alamat e-mel: '; +$PHPMAILER_LANG['instantiate'] = 'Tidak dapat memberi contoh fungsi e-mel.'; +$PHPMAILER_LANG['invalid_address'] = 'Alamat emel tidak sah: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' jenis penghantar emel tidak disokong.'; +$PHPMAILER_LANG['provide_address'] = 'Anda perlu menyediakan sekurang-kurangnya satu alamat e-mel penerima.'; +$PHPMAILER_LANG['recipients_failed'] = 'Ralat SMTP: Penerima e-mel berikut telah gagal: '; +$PHPMAILER_LANG['signing'] = 'Ralat pada tanda tangan: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() telah gagal.'; +$PHPMAILER_LANG['smtp_error'] = 'Ralat pada pelayan SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Tidak boleh menetapkan atau menetapkan semula pembolehubah: '; +$PHPMAILER_LANG['extension_missing'] = 'Sambungan hilang: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-nb.php b/orcinus/phpmailer/language/phpmailer.lang-nb.php new file mode 100644 index 0000000..65793ce --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-nb.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP-fout: authenticatie mislukt.'; +$PHPMAILER_LANG['buggy_php'] = 'PHP versie gededecteerd die onderhavig is aan een bug die kan resulteren in gecorrumpeerde berichten. Om dit te voorkomen, gebruik SMTP voor het verzenden van berichten, zet de mail.add_x_header optie in uw php.ini file uit, gebruik MacOS of Linux, of pas de gebruikte PHP versie aan naar versie 7.0.17+ or 7.1.3+.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP-fout: kon niet verbinden met SMTP-host.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP-fout: data niet geaccepteerd.'; +$PHPMAILER_LANG['empty_message'] = 'Berichttekst is leeg'; +$PHPMAILER_LANG['encoding'] = 'Onbekende codering: '; +$PHPMAILER_LANG['execute'] = 'Kon niet uitvoeren: '; +$PHPMAILER_LANG['extension_missing'] = 'Extensie afwezig: '; +$PHPMAILER_LANG['file_access'] = 'Kreeg geen toegang tot bestand: '; +$PHPMAILER_LANG['file_open'] = 'Bestandsfout: kon bestand niet openen: '; +$PHPMAILER_LANG['from_failed'] = 'Het volgende afzendersadres is mislukt: '; +$PHPMAILER_LANG['instantiate'] = 'Kon mailfunctie niet initialiseren.'; +$PHPMAILER_LANG['invalid_address'] = 'Ongeldig adres: '; +$PHPMAILER_LANG['invalid_header'] = 'Ongeldige header naam of waarde'; +$PHPMAILER_LANG['invalid_hostentry'] = 'Ongeldige hostentry: '; +$PHPMAILER_LANG['invalid_host'] = 'Ongeldige host: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer wordt niet ondersteund.'; +$PHPMAILER_LANG['provide_address'] = 'Er moet minstens één ontvanger worden opgegeven.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP-fout: de volgende ontvangers zijn mislukt: '; +$PHPMAILER_LANG['signing'] = 'Signeerfout: '; +$PHPMAILER_LANG['smtp_code'] = 'SMTP code: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'Aanvullende SMTP informatie: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Verbinding mislukt.'; +$PHPMAILER_LANG['smtp_detail'] = 'Detail: '; +$PHPMAILER_LANG['smtp_error'] = 'SMTP-serverfout: '; +$PHPMAILER_LANG['variable_set'] = 'Kan de volgende variabele niet instellen of resetten: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-pl.php b/orcinus/phpmailer/language/phpmailer.lang-pl.php new file mode 100644 index 0000000..b0469fd --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-pl.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Erro do SMTP: Não foi possível realizar a autenticação.'; +$PHPMAILER_LANG['connect_host'] = 'Erro do SMTP: Não foi possível realizar ligação com o servidor SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Erro do SMTP: Os dados foram rejeitados.'; +$PHPMAILER_LANG['empty_message'] = 'A mensagem no e-mail está vazia.'; +$PHPMAILER_LANG['encoding'] = 'Codificação desconhecida: '; +$PHPMAILER_LANG['execute'] = 'Não foi possível executar: '; +$PHPMAILER_LANG['file_access'] = 'Não foi possível aceder o ficheiro: '; +$PHPMAILER_LANG['file_open'] = 'Abertura do ficheiro: Não foi possível abrir o ficheiro: '; +$PHPMAILER_LANG['from_failed'] = 'Ocorreram falhas nos endereços dos seguintes remententes: '; +$PHPMAILER_LANG['instantiate'] = 'Não foi possível iniciar uma instância da função mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Não foi enviado nenhum e-mail para o endereço de e-mail inválido: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer não é suportado.'; +$PHPMAILER_LANG['provide_address'] = 'Tem de fornecer pelo menos um endereço como destinatário do e-mail.'; +$PHPMAILER_LANG['recipients_failed'] = 'Erro do SMTP: O endereço do seguinte destinatário falhou: '; +$PHPMAILER_LANG['signing'] = 'Erro ao assinar: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falhou.'; +$PHPMAILER_LANG['smtp_error'] = 'Erro de servidor SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Não foi possível definir ou redefinir a variável: '; +$PHPMAILER_LANG['extension_missing'] = 'Extensão em falta: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-pt_br.php b/orcinus/phpmailer/language/phpmailer.lang-pt_br.php new file mode 100644 index 0000000..5239865 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-pt_br.php @@ -0,0 +1,38 @@ + + * @author Lucas Guimarães + * @author Phelipe Alves + * @author Fabio Beneditto + * @author Geidson Benício Coelho + */ + +$PHPMAILER_LANG['authenticate'] = 'Erro de SMTP: Não foi possível autenticar.'; +$PHPMAILER_LANG['buggy_php'] = 'Sua versão do PHP é afetada por um bug que por resultar em messagens corrompidas. Para corrigir, mude para enviar usando SMTP, desative a opção mail.add_x_header em seu php.ini, mude para MacOS ou Linux, ou atualize seu PHP para versão 7.0.17+ ou 7.1.3+ '; +$PHPMAILER_LANG['connect_host'] = 'Erro de SMTP: Não foi possível conectar ao servidor SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Erro de SMTP: Dados rejeitados.'; +$PHPMAILER_LANG['empty_message'] = 'Mensagem vazia'; +$PHPMAILER_LANG['encoding'] = 'Codificação desconhecida: '; +$PHPMAILER_LANG['execute'] = 'Não foi possível executar: '; +$PHPMAILER_LANG['extension_missing'] = 'Extensão não existe: '; +$PHPMAILER_LANG['file_access'] = 'Não foi possível acessar o arquivo: '; +$PHPMAILER_LANG['file_open'] = 'Erro de Arquivo: Não foi possível abrir o arquivo: '; +$PHPMAILER_LANG['from_failed'] = 'Os seguintes remetentes falharam: '; +$PHPMAILER_LANG['instantiate'] = 'Não foi possível instanciar a função mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Endereço de e-mail inválido: '; +$PHPMAILER_LANG['invalid_header'] = 'Nome ou valor de cabeçalho inválido'; +$PHPMAILER_LANG['invalid_hostentry'] = 'hostentry inválido: '; +$PHPMAILER_LANG['invalid_host'] = 'host inválido: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer não é suportado.'; +$PHPMAILER_LANG['provide_address'] = 'Você deve informar pelo menos um destinatário.'; +$PHPMAILER_LANG['recipients_failed'] = 'Erro de SMTP: Os seguintes destinatários falharam: '; +$PHPMAILER_LANG['signing'] = 'Erro de Assinatura: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falhou.'; +$PHPMAILER_LANG['smtp_code'] = 'Código do servidor SMTP: '; +$PHPMAILER_LANG['smtp_error'] = 'Erro de servidor SMTP: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'Informações adicionais do servidor SMTP: '; +$PHPMAILER_LANG['smtp_detail'] = 'Detalhes do servidor SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Não foi possível definir ou redefinir a variável: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-ro.php b/orcinus/phpmailer/language/phpmailer.lang-ro.php new file mode 100644 index 0000000..45bef91 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-ro.php @@ -0,0 +1,33 @@ + + * @author Foster Snowhill + */ + +$PHPMAILER_LANG['authenticate'] = 'Ошибка SMTP: ошибка авторизации.'; +$PHPMAILER_LANG['connect_host'] = 'Ошибка SMTP: не удается подключиться к SMTP-серверу.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Ошибка SMTP: данные не приняты.'; +$PHPMAILER_LANG['encoding'] = 'Неизвестная кодировка: '; +$PHPMAILER_LANG['execute'] = 'Невозможно выполнить команду: '; +$PHPMAILER_LANG['file_access'] = 'Нет доступа к файлу: '; +$PHPMAILER_LANG['file_open'] = 'Файловая ошибка: не удаётся открыть файл: '; +$PHPMAILER_LANG['from_failed'] = 'Неверный адрес отправителя: '; +$PHPMAILER_LANG['instantiate'] = 'Невозможно запустить функцию mail().'; +$PHPMAILER_LANG['provide_address'] = 'Пожалуйста, введите хотя бы один email-адрес получателя.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' — почтовый сервер не поддерживается.'; +$PHPMAILER_LANG['recipients_failed'] = 'Ошибка SMTP: не удалась отправка таким адресатам: '; +$PHPMAILER_LANG['empty_message'] = 'Пустое сообщение'; +$PHPMAILER_LANG['invalid_address'] = 'Не отправлено из-за неправильного формата email-адреса: '; +$PHPMAILER_LANG['signing'] = 'Ошибка подписи: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Ошибка соединения с SMTP-сервером'; +$PHPMAILER_LANG['smtp_error'] = 'Ошибка SMTP-сервера: '; +$PHPMAILER_LANG['variable_set'] = 'Невозможно установить или сбросить переменную: '; +$PHPMAILER_LANG['extension_missing'] = 'Расширение отсутствует: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-sk.php b/orcinus/phpmailer/language/phpmailer.lang-sk.php new file mode 100644 index 0000000..028f5bc --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-sk.php @@ -0,0 +1,30 @@ + + * @author Peter Orlický + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Error: Chyba autentifikácie.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Nebolo možné nadviazať spojenie so SMTP serverom.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Error: Dáta neboli prijaté'; +$PHPMAILER_LANG['empty_message'] = 'Prázdne telo správy.'; +$PHPMAILER_LANG['encoding'] = 'Neznáme kódovanie: '; +$PHPMAILER_LANG['execute'] = 'Nedá sa vykonať: '; +$PHPMAILER_LANG['file_access'] = 'Súbor nebol nájdený: '; +$PHPMAILER_LANG['file_open'] = 'File Error: Súbor sa otvoriť pre čítanie: '; +$PHPMAILER_LANG['from_failed'] = 'Následujúca adresa From je nesprávna: '; +$PHPMAILER_LANG['instantiate'] = 'Nedá sa vytvoriť inštancia emailovej funkcie.'; +$PHPMAILER_LANG['invalid_address'] = 'Neodoslané, emailová adresa je nesprávna: '; +$PHPMAILER_LANG['invalid_hostentry'] = 'Záznam hostiteľa je nesprávny: '; +$PHPMAILER_LANG['invalid_host'] = 'Hostiteľ je nesprávny: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' emailový klient nieje podporovaný.'; +$PHPMAILER_LANG['provide_address'] = 'Musíte zadať aspoň jednu emailovú adresu príjemcu.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: Adresy príjemcov niesu správne '; +$PHPMAILER_LANG['signing'] = 'Chyba prihlasovania: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() zlyhalo.'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP chyba serveru: '; +$PHPMAILER_LANG['variable_set'] = 'Nemožno nastaviť alebo resetovať premennú: '; +$PHPMAILER_LANG['extension_missing'] = 'Chýba rozšírenie: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-sl.php b/orcinus/phpmailer/language/phpmailer.lang-sl.php new file mode 100644 index 0000000..3e00c25 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-sl.php @@ -0,0 +1,36 @@ + + * @author Filip Š + * @author Blaž Oražem + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP napaka: Avtentikacija ni uspela.'; +$PHPMAILER_LANG['buggy_php'] = 'Na vašo PHP različico vpliva napaka, ki lahko povzroči poškodovana sporočila. Če želite težavo odpraviti, preklopite na pošiljanje prek SMTP, onemogočite možnost mail.add_x_header v vaši php.ini datoteki, preklopite na MacOS ali Linux, ali nadgradite vašo PHP zaličico na 7.0.17+ ali 7.1.3+.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP napaka: Vzpostavljanje povezave s SMTP gostiteljem ni uspelo.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP napaka: Strežnik zavrača podatke.'; +$PHPMAILER_LANG['empty_message'] = 'E-poštno sporočilo nima vsebine.'; +$PHPMAILER_LANG['encoding'] = 'Nepoznan tip kodiranja: '; +$PHPMAILER_LANG['execute'] = 'Operacija ni uspela: '; +$PHPMAILER_LANG['extension_missing'] = 'Manjkajoča razširitev: '; +$PHPMAILER_LANG['file_access'] = 'Nimam dostopa do datoteke: '; +$PHPMAILER_LANG['file_open'] = 'Ne morem odpreti datoteke: '; +$PHPMAILER_LANG['from_failed'] = 'Neveljaven e-naslov pošiljatelja: '; +$PHPMAILER_LANG['instantiate'] = 'Ne morem inicializirati mail funkcije.'; +$PHPMAILER_LANG['invalid_address'] = 'E-poštno sporočilo ni bilo poslano. E-naslov je neveljaven: '; +$PHPMAILER_LANG['invalid_header'] = 'Neveljavno ime ali vrednost glave'; +$PHPMAILER_LANG['invalid_hostentry'] = 'Neveljaven vnos gostitelja: '; +$PHPMAILER_LANG['invalid_host'] = 'Neveljaven gostitelj: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer ni podprt.'; +$PHPMAILER_LANG['provide_address'] = 'Prosimo, vnesite vsaj enega naslovnika.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP napaka: Sledeči naslovniki so neveljavni: '; +$PHPMAILER_LANG['signing'] = 'Napaka pri podpisovanju: '; +$PHPMAILER_LANG['smtp_code'] = 'SMTP koda: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'Dodatne informacije o SMTP: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Ne morem vzpostaviti povezave s SMTP strežnikom.'; +$PHPMAILER_LANG['smtp_detail'] = 'Podrobnosti: '; +$PHPMAILER_LANG['smtp_error'] = 'Napaka SMTP strežnika: '; +$PHPMAILER_LANG['variable_set'] = 'Ne morem nastaviti oz. ponastaviti spremenljivke: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-sr.php b/orcinus/phpmailer/language/phpmailer.lang-sr.php new file mode 100644 index 0000000..0b5280f --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-sr.php @@ -0,0 +1,28 @@ + + * @author Miloš Milanović + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP грешка: аутентификација није успела.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP грешка: повезивање са SMTP сервером није успело.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP грешка: подаци нису прихваћени.'; +$PHPMAILER_LANG['empty_message'] = 'Садржај поруке је празан.'; +$PHPMAILER_LANG['encoding'] = 'Непознато кодирање: '; +$PHPMAILER_LANG['execute'] = 'Није могуће извршити наредбу: '; +$PHPMAILER_LANG['file_access'] = 'Није могуће приступити датотеци: '; +$PHPMAILER_LANG['file_open'] = 'Није могуће отворити датотеку: '; +$PHPMAILER_LANG['from_failed'] = 'SMTP грешка: слање са следећих адреса није успело: '; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP грешка: слање на следеће адресе није успело: '; +$PHPMAILER_LANG['instantiate'] = 'Није могуће покренути mail функцију.'; +$PHPMAILER_LANG['invalid_address'] = 'Порука није послата. Неисправна адреса: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' мејлер није подржан.'; +$PHPMAILER_LANG['provide_address'] = 'Дефинишите бар једну адресу примаоца.'; +$PHPMAILER_LANG['signing'] = 'Грешка приликом пријаве: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Повезивање са SMTP сервером није успело.'; +$PHPMAILER_LANG['smtp_error'] = 'Грешка SMTP сервера: '; +$PHPMAILER_LANG['variable_set'] = 'Није могуће задати нити ресетовати променљиву: '; +$PHPMAILER_LANG['extension_missing'] = 'Недостаје проширење: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-sr_latn.php b/orcinus/phpmailer/language/phpmailer.lang-sr_latn.php new file mode 100644 index 0000000..6213832 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-sr_latn.php @@ -0,0 +1,28 @@ + + * @author Miloš Milanović + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP greška: autentifikacija nije uspela.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP greška: povezivanje sa SMTP serverom nije uspelo.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP greška: podaci nisu prihvaćeni.'; +$PHPMAILER_LANG['empty_message'] = 'Sadržaj poruke je prazan.'; +$PHPMAILER_LANG['encoding'] = 'Nepoznato kodiranje: '; +$PHPMAILER_LANG['execute'] = 'Nije moguće izvršiti naredbu: '; +$PHPMAILER_LANG['file_access'] = 'Nije moguće pristupiti datoteci: '; +$PHPMAILER_LANG['file_open'] = 'Nije moguće otvoriti datoteku: '; +$PHPMAILER_LANG['from_failed'] = 'SMTP greška: slanje sa sledećih adresa nije uspelo: '; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP greška: slanje na sledeće adrese nije uspelo: '; +$PHPMAILER_LANG['instantiate'] = 'Nije moguće pokrenuti mail funkciju.'; +$PHPMAILER_LANG['invalid_address'] = 'Poruka nije poslata. Neispravna adresa: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' majler nije podržan.'; +$PHPMAILER_LANG['provide_address'] = 'Definišite bar jednu adresu primaoca.'; +$PHPMAILER_LANG['signing'] = 'Greška prilikom prijave: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Povezivanje sa SMTP serverom nije uspelo.'; +$PHPMAILER_LANG['smtp_error'] = 'Greška SMTP servera: '; +$PHPMAILER_LANG['variable_set'] = 'Nije moguće zadati niti resetovati promenljivu: '; +$PHPMAILER_LANG['extension_missing'] = 'Nedostaje proširenje: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-sv.php b/orcinus/phpmailer/language/phpmailer.lang-sv.php new file mode 100644 index 0000000..9872c19 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-sv.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP fel: Kunde inte autentisera.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP fel: Kunde inte ansluta till SMTP-server.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP fel: Data accepterades inte.'; +//$PHPMAILER_LANG['empty_message'] = 'Message body empty'; +$PHPMAILER_LANG['encoding'] = 'Okänt encode-format: '; +$PHPMAILER_LANG['execute'] = 'Kunde inte köra: '; +$PHPMAILER_LANG['file_access'] = 'Ingen åtkomst till fil: '; +$PHPMAILER_LANG['file_open'] = 'Fil fel: Kunde inte öppna fil: '; +$PHPMAILER_LANG['from_failed'] = 'Följande avsändaradress är felaktig: '; +$PHPMAILER_LANG['instantiate'] = 'Kunde inte initiera e-postfunktion.'; +$PHPMAILER_LANG['invalid_address'] = 'Felaktig adress: '; +$PHPMAILER_LANG['provide_address'] = 'Du måste ange minst en mottagares e-postadress.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer stöds inte.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP fel: Följande mottagare är felaktig: '; +$PHPMAILER_LANG['signing'] = 'Signeringsfel: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() misslyckades.'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP serverfel: '; +$PHPMAILER_LANG['variable_set'] = 'Kunde inte definiera eller återställa variabel: '; +$PHPMAILER_LANG['extension_missing'] = 'Tillägg ej tillgängligt: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-tl.php b/orcinus/phpmailer/language/phpmailer.lang-tl.php new file mode 100644 index 0000000..d15bed1 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-tl.php @@ -0,0 +1,28 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Error: Hindi mapatotohanan.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Hindi makakonekta sa SMTP host.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Error: Ang datos ay hindi naitanggap.'; +$PHPMAILER_LANG['empty_message'] = 'Walang laman ang mensahe'; +$PHPMAILER_LANG['encoding'] = 'Hindi alam ang encoding: '; +$PHPMAILER_LANG['execute'] = 'Hindi maisasagawa: '; +$PHPMAILER_LANG['file_access'] = 'Hindi ma-access ang file: '; +$PHPMAILER_LANG['file_open'] = 'File Error: Hindi mabuksan ang file: '; +$PHPMAILER_LANG['from_failed'] = 'Ang sumusunod na address ay nabigo: '; +$PHPMAILER_LANG['instantiate'] = 'Hindi maisimulan ang instance ng mail function.'; +$PHPMAILER_LANG['invalid_address'] = 'Hindi wasto ang address na naibigay: '; +$PHPMAILER_LANG['mailer_not_supported'] = 'Ang mailer ay hindi suportado.'; +$PHPMAILER_LANG['provide_address'] = 'Kailangan mong magbigay ng kahit isang email address na tatanggap.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: Ang mga sumusunod na tatanggap ay nabigo: '; +$PHPMAILER_LANG['signing'] = 'Hindi ma-sign: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Ang SMTP connect() ay nabigo.'; +$PHPMAILER_LANG['smtp_error'] = 'Ang server ng SMTP ay nabigo: '; +$PHPMAILER_LANG['variable_set'] = 'Hindi matatakda o ma-reset ang mga variables: '; +$PHPMAILER_LANG['extension_missing'] = 'Nawawala ang extension: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-tr.php b/orcinus/phpmailer/language/phpmailer.lang-tr.php new file mode 100644 index 0000000..f938f80 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-tr.php @@ -0,0 +1,31 @@ + + * @fixed by Boris Yurchenko + */ + +$PHPMAILER_LANG['authenticate'] = 'Помилка SMTP: помилка авторизації.'; +$PHPMAILER_LANG['connect_host'] = 'Помилка SMTP: не вдається під\'єднатися до SMTP-серверу.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Помилка SMTP: дані не прийнято.'; +$PHPMAILER_LANG['encoding'] = 'Невідоме кодування: '; +$PHPMAILER_LANG['execute'] = 'Неможливо виконати команду: '; +$PHPMAILER_LANG['file_access'] = 'Немає доступу до файлу: '; +$PHPMAILER_LANG['file_open'] = 'Помилка файлової системи: не вдається відкрити файл: '; +$PHPMAILER_LANG['from_failed'] = 'Невірна адреса відправника: '; +$PHPMAILER_LANG['instantiate'] = 'Неможливо запустити функцію mail().'; +$PHPMAILER_LANG['provide_address'] = 'Будь ласка, введіть хоча б одну email-адресу отримувача.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' - поштовий сервер не підтримується.'; +$PHPMAILER_LANG['recipients_failed'] = 'Помилка SMTP: не вдалося відправлення для таких отримувачів: '; +$PHPMAILER_LANG['empty_message'] = 'Пусте повідомлення'; +$PHPMAILER_LANG['invalid_address'] = 'Не відправлено через неправильний формат email-адреси: '; +$PHPMAILER_LANG['signing'] = 'Помилка підпису: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Помилка з\'єднання з SMTP-сервером'; +$PHPMAILER_LANG['smtp_error'] = 'Помилка SMTP-сервера: '; +$PHPMAILER_LANG['variable_set'] = 'Неможливо встановити або скинути змінну: '; +$PHPMAILER_LANG['extension_missing'] = 'Розширення відсутнє: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-vi.php b/orcinus/phpmailer/language/phpmailer.lang-vi.php new file mode 100644 index 0000000..d65576e --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-vi.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Lỗi SMTP: Không thể xác thực.'; +$PHPMAILER_LANG['connect_host'] = 'Lỗi SMTP: Không thể kết nối máy chủ SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Lỗi SMTP: Dữ liệu không được chấp nhận.'; +$PHPMAILER_LANG['empty_message'] = 'Không có nội dung'; +$PHPMAILER_LANG['encoding'] = 'Mã hóa không xác định: '; +$PHPMAILER_LANG['execute'] = 'Không thực hiện được: '; +$PHPMAILER_LANG['file_access'] = 'Không thể truy cập tệp tin '; +$PHPMAILER_LANG['file_open'] = 'Lỗi Tập tin: Không thể mở tệp tin: '; +$PHPMAILER_LANG['from_failed'] = 'Lỗi địa chỉ gửi đi: '; +$PHPMAILER_LANG['instantiate'] = 'Không dùng được các hàm gửi thư.'; +$PHPMAILER_LANG['invalid_address'] = 'Đại chỉ emai không đúng: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' trình gửi thư không được hỗ trợ.'; +$PHPMAILER_LANG['provide_address'] = 'Bạn phải cung cấp ít nhất một địa chỉ người nhận.'; +$PHPMAILER_LANG['recipients_failed'] = 'Lỗi SMTP: lỗi địa chỉ người nhận: '; +$PHPMAILER_LANG['signing'] = 'Lỗi đăng nhập: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Lỗi kết nối với SMTP'; +$PHPMAILER_LANG['smtp_error'] = 'Lỗi máy chủ smtp '; +$PHPMAILER_LANG['variable_set'] = 'Không thể thiết lập hoặc thiết lập lại biến: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-zh.php b/orcinus/phpmailer/language/phpmailer.lang-zh.php new file mode 100644 index 0000000..35e4e70 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-zh.php @@ -0,0 +1,29 @@ + + * @author Peter Dave Hello <@PeterDaveHello/> + * @author Jason Chiang + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP 錯誤:登入失敗。'; +$PHPMAILER_LANG['connect_host'] = 'SMTP 錯誤:無法連線到 SMTP 主機。'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP 錯誤:無法接受的資料。'; +$PHPMAILER_LANG['empty_message'] = '郵件內容為空'; +$PHPMAILER_LANG['encoding'] = '未知編碼: '; +$PHPMAILER_LANG['execute'] = '無法執行:'; +$PHPMAILER_LANG['file_access'] = '無法存取檔案:'; +$PHPMAILER_LANG['file_open'] = '檔案錯誤:無法開啟檔案:'; +$PHPMAILER_LANG['from_failed'] = '發送地址錯誤:'; +$PHPMAILER_LANG['instantiate'] = '未知函數呼叫。'; +$PHPMAILER_LANG['invalid_address'] = '因為電子郵件地址無效,無法傳送: '; +$PHPMAILER_LANG['mailer_not_supported'] = '不支援的發信客戶端。'; +$PHPMAILER_LANG['provide_address'] = '必須提供至少一個收件人地址。'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP 錯誤:以下收件人地址錯誤:'; +$PHPMAILER_LANG['signing'] = '電子簽章錯誤: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP 連線失敗'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP 伺服器錯誤: '; +$PHPMAILER_LANG['variable_set'] = '無法設定或重設變數: '; +$PHPMAILER_LANG['extension_missing'] = '遺失模組 Extension: '; diff --git a/orcinus/phpmailer/language/phpmailer.lang-zh_cn.php b/orcinus/phpmailer/language/phpmailer.lang-zh_cn.php new file mode 100644 index 0000000..728a499 --- /dev/null +++ b/orcinus/phpmailer/language/phpmailer.lang-zh_cn.php @@ -0,0 +1,29 @@ + + * @author young + * @author Teddysun + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP 错误:登录失败。'; +$PHPMAILER_LANG['connect_host'] = 'SMTP 错误:无法连接到 SMTP 主机。'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP 错误:数据不被接受。'; +$PHPMAILER_LANG['empty_message'] = '邮件正文为空。'; +$PHPMAILER_LANG['encoding'] = '未知编码:'; +$PHPMAILER_LANG['execute'] = '无法执行:'; +$PHPMAILER_LANG['file_access'] = '无法访问文件:'; +$PHPMAILER_LANG['file_open'] = '文件错误:无法打开文件:'; +$PHPMAILER_LANG['from_failed'] = '发送地址错误:'; +$PHPMAILER_LANG['instantiate'] = '未知函数调用。'; +$PHPMAILER_LANG['invalid_address'] = '发送失败,电子邮箱地址是无效的:'; +$PHPMAILER_LANG['mailer_not_supported'] = '发信客户端不被支持。'; +$PHPMAILER_LANG['provide_address'] = '必须提供至少一个收件人地址。'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP 错误:收件人地址错误:'; +$PHPMAILER_LANG['signing'] = '登录失败:'; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP服务器连接失败。'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP服务器出错:'; +$PHPMAILER_LANG['variable_set'] = '无法设置或重置变量:'; +$PHPMAILER_LANG['extension_missing'] = '丢失模块 Extension:'; diff --git a/orcinus/phpmailer/src/DSNConfigurator.php b/orcinus/phpmailer/src/DSNConfigurator.php new file mode 100644 index 0000000..ab707d2 --- /dev/null +++ b/orcinus/phpmailer/src/DSNConfigurator.php @@ -0,0 +1,247 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2023 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +/** + * Configure PHPMailer with DSN string. + * + * @see https://en.wikipedia.org/wiki/Data_source_name + * + * @author Oleg Voronkovich + */ +class DSNConfigurator +{ + /** + * Create new PHPMailer instance configured by DSN. + * + * @param string $dsn DSN + * @param bool $exceptions Should we throw external exceptions? + * + * @return PHPMailer + */ + public static function mailer($dsn, $exceptions = null) + { + static $configurator = null; + + if (null === $configurator) { + $configurator = new DSNConfigurator(); + } + + return $configurator->configure(new PHPMailer($exceptions), $dsn); + } + + /** + * Configure PHPMailer instance with DSN string. + * + * @param PHPMailer $mailer PHPMailer instance + * @param string $dsn DSN + * + * @return PHPMailer + */ + public function configure(PHPMailer $mailer, $dsn) + { + $config = $this->parseDSN($dsn); + + $this->applyConfig($mailer, $config); + + return $mailer; + } + + /** + * Parse DSN string. + * + * @param string $dsn DSN + * + * @throws Exception If DSN is malformed + * + * @return array Configuration + */ + private function parseDSN($dsn) + { + $config = $this->parseUrl($dsn); + + if (false === $config || !isset($config['scheme']) || !isset($config['host'])) { + throw new Exception( + sprintf('Malformed DSN: "%s".', $dsn) + ); + } + + if (isset($config['query'])) { + parse_str($config['query'], $config['query']); + } + + return $config; + } + + /** + * Apply configuration to mailer. + * + * @param PHPMailer $mailer PHPMailer instance + * @param array $config Configuration + * + * @throws Exception If scheme is invalid + */ + private function applyConfig(PHPMailer $mailer, $config) + { + switch ($config['scheme']) { + case 'mail': + $mailer->isMail(); + break; + case 'sendmail': + $mailer->isSendmail(); + break; + case 'qmail': + $mailer->isQmail(); + break; + case 'smtp': + case 'smtps': + $mailer->isSMTP(); + $this->configureSMTP($mailer, $config); + break; + default: + throw new Exception( + sprintf( + 'Invalid scheme: "%s". Allowed values: "mail", "sendmail", "qmail", "smtp", "smtps".', + $config['scheme'] + ) + ); + } + + if (isset($config['query'])) { + $this->configureOptions($mailer, $config['query']); + } + } + + /** + * Configure SMTP. + * + * @param PHPMailer $mailer PHPMailer instance + * @param array $config Configuration + */ + private function configureSMTP($mailer, $config) + { + $isSMTPS = 'smtps' === $config['scheme']; + + if ($isSMTPS) { + $mailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; + } + + $mailer->Host = $config['host']; + + if (isset($config['port'])) { + $mailer->Port = $config['port']; + } elseif ($isSMTPS) { + $mailer->Port = SMTP::DEFAULT_SECURE_PORT; + } + + $mailer->SMTPAuth = isset($config['user']) || isset($config['pass']); + + if (isset($config['user'])) { + $mailer->Username = $config['user']; + } + + if (isset($config['pass'])) { + $mailer->Password = $config['pass']; + } + } + + /** + * Configure options. + * + * @param PHPMailer $mailer PHPMailer instance + * @param array $options Options + * + * @throws Exception If option is unknown + */ + private function configureOptions(PHPMailer $mailer, $options) + { + $allowedOptions = get_object_vars($mailer); + + unset($allowedOptions['Mailer']); + unset($allowedOptions['SMTPAuth']); + unset($allowedOptions['Username']); + unset($allowedOptions['Password']); + unset($allowedOptions['Hostname']); + unset($allowedOptions['Port']); + unset($allowedOptions['ErrorInfo']); + + $allowedOptions = \array_keys($allowedOptions); + + foreach ($options as $key => $value) { + if (!in_array($key, $allowedOptions)) { + throw new Exception( + sprintf( + 'Unknown option: "%s". Allowed values: "%s"', + $key, + implode('", "', $allowedOptions) + ) + ); + } + + switch ($key) { + case 'AllowEmpty': + case 'SMTPAutoTLS': + case 'SMTPKeepAlive': + case 'SingleTo': + case 'UseSendmailOptions': + case 'do_verp': + case 'DKIM_copyHeaderFields': + $mailer->$key = (bool) $value; + break; + case 'Priority': + case 'SMTPDebug': + case 'WordWrap': + $mailer->$key = (int) $value; + break; + default: + $mailer->$key = $value; + break; + } + } + } + + /** + * Parse a URL. + * Wrapper for the built-in parse_url function to work around a bug in PHP 5.5. + * + * @param string $url URL + * + * @return array|false + */ + protected function parseUrl($url) + { + if (\PHP_VERSION_ID >= 50600 || false === strpos($url, '?')) { + return parse_url($url); + } + + $chunks = explode('?', $url); + if (is_array($chunks)) { + $result = parse_url($chunks[0]); + if (is_array($result)) { + $result['query'] = $chunks[1]; + } + return $result; + } + + return false; + } +} diff --git a/orcinus/phpmailer/src/Exception.php b/orcinus/phpmailer/src/Exception.php new file mode 100644 index 0000000..52eaf95 --- /dev/null +++ b/orcinus/phpmailer/src/Exception.php @@ -0,0 +1,40 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2020 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +/** + * PHPMailer exception handler. + * + * @author Marcus Bointon + */ +class Exception extends \Exception +{ + /** + * Prettify error message output. + * + * @return string + */ + public function errorMessage() + { + return '' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . "
\n"; + } +} diff --git a/orcinus/phpmailer/src/OAuth.php b/orcinus/phpmailer/src/OAuth.php new file mode 100644 index 0000000..c1d5b77 --- /dev/null +++ b/orcinus/phpmailer/src/OAuth.php @@ -0,0 +1,139 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2020 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +use League\OAuth2\Client\Grant\RefreshToken; +use League\OAuth2\Client\Provider\AbstractProvider; +use League\OAuth2\Client\Token\AccessToken; + +/** + * OAuth - OAuth2 authentication wrapper class. + * Uses the oauth2-client package from the League of Extraordinary Packages. + * + * @see http://oauth2-client.thephpleague.com + * + * @author Marcus Bointon (Synchro/coolbru) + */ +class OAuth implements OAuthTokenProvider +{ + /** + * An instance of the League OAuth Client Provider. + * + * @var AbstractProvider + */ + protected $provider; + + /** + * The current OAuth access token. + * + * @var AccessToken + */ + protected $oauthToken; + + /** + * The user's email address, usually used as the login ID + * and also the from address when sending email. + * + * @var string + */ + protected $oauthUserEmail = ''; + + /** + * The client secret, generated in the app definition of the service you're connecting to. + * + * @var string + */ + protected $oauthClientSecret = ''; + + /** + * The client ID, generated in the app definition of the service you're connecting to. + * + * @var string + */ + protected $oauthClientId = ''; + + /** + * The refresh token, used to obtain new AccessTokens. + * + * @var string + */ + protected $oauthRefreshToken = ''; + + /** + * OAuth constructor. + * + * @param array $options Associative array containing + * `provider`, `userName`, `clientSecret`, `clientId` and `refreshToken` elements + */ + public function __construct($options) + { + $this->provider = $options['provider']; + $this->oauthUserEmail = $options['userName']; + $this->oauthClientSecret = $options['clientSecret']; + $this->oauthClientId = $options['clientId']; + $this->oauthRefreshToken = $options['refreshToken']; + } + + /** + * Get a new RefreshToken. + * + * @return RefreshToken + */ + protected function getGrant() + { + return new RefreshToken(); + } + + /** + * Get a new AccessToken. + * + * @return AccessToken + */ + protected function getToken() + { + return $this->provider->getAccessToken( + $this->getGrant(), + ['refresh_token' => $this->oauthRefreshToken] + ); + } + + /** + * Generate a base64-encoded OAuth token. + * + * @return string + */ + public function getOauth64() + { + //Get a new token if it's not available or has expired + if (null === $this->oauthToken || $this->oauthToken->hasExpired()) { + $this->oauthToken = $this->getToken(); + } + + return base64_encode( + 'user=' . + $this->oauthUserEmail . + "\001auth=Bearer " . + $this->oauthToken . + "\001\001" + ); + } +} diff --git a/orcinus/phpmailer/src/OAuthTokenProvider.php b/orcinus/phpmailer/src/OAuthTokenProvider.php new file mode 100644 index 0000000..1155507 --- /dev/null +++ b/orcinus/phpmailer/src/OAuthTokenProvider.php @@ -0,0 +1,44 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2020 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +/** + * OAuthTokenProvider - OAuth2 token provider interface. + * Provides base64 encoded OAuth2 auth strings for SMTP authentication. + * + * @see OAuth + * @see SMTP::authenticate() + * + * @author Peter Scopes (pdscopes) + * @author Marcus Bointon (Synchro/coolbru) + */ +interface OAuthTokenProvider +{ + /** + * Generate a base64-encoded OAuth token ensuring that the access token has not expired. + * The string to be base 64 encoded should be in the form: + * "user=\001auth=Bearer \001\001" + * + * @return string + */ + public function getOauth64(); +} diff --git a/orcinus/phpmailer/src/PHPMailer.php b/orcinus/phpmailer/src/PHPMailer.php new file mode 100644 index 0000000..a644d2c --- /dev/null +++ b/orcinus/phpmailer/src/PHPMailer.php @@ -0,0 +1,5126 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2020 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +/** + * PHPMailer - PHP email creation and transport class. + * + * @author Marcus Bointon (Synchro/coolbru) + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + */ +class PHPMailer +{ + const CHARSET_ASCII = 'us-ascii'; + const CHARSET_ISO88591 = 'iso-8859-1'; + const CHARSET_UTF8 = 'utf-8'; + + const CONTENT_TYPE_PLAINTEXT = 'text/plain'; + const CONTENT_TYPE_TEXT_CALENDAR = 'text/calendar'; + const CONTENT_TYPE_TEXT_HTML = 'text/html'; + const CONTENT_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative'; + const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed'; + const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related'; + + const ENCODING_7BIT = '7bit'; + const ENCODING_8BIT = '8bit'; + const ENCODING_BASE64 = 'base64'; + const ENCODING_BINARY = 'binary'; + const ENCODING_QUOTED_PRINTABLE = 'quoted-printable'; + + const ENCRYPTION_STARTTLS = 'tls'; + const ENCRYPTION_SMTPS = 'ssl'; + + const ICAL_METHOD_REQUEST = 'REQUEST'; + const ICAL_METHOD_PUBLISH = 'PUBLISH'; + const ICAL_METHOD_REPLY = 'REPLY'; + const ICAL_METHOD_ADD = 'ADD'; + const ICAL_METHOD_CANCEL = 'CANCEL'; + const ICAL_METHOD_REFRESH = 'REFRESH'; + const ICAL_METHOD_COUNTER = 'COUNTER'; + const ICAL_METHOD_DECLINECOUNTER = 'DECLINECOUNTER'; + + /** + * Email priority. + * Options: null (default), 1 = High, 3 = Normal, 5 = low. + * When null, the header is not set at all. + * + * @var int|null + */ + public $Priority; + + /** + * The character set of the message. + * + * @var string + */ + public $CharSet = self::CHARSET_ISO88591; + + /** + * The MIME Content-type of the message. + * + * @var string + */ + public $ContentType = self::CONTENT_TYPE_PLAINTEXT; + + /** + * The message encoding. + * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable". + * + * @var string + */ + public $Encoding = self::ENCODING_8BIT; + + /** + * Holds the most recent mailer error message. + * + * @var string + */ + public $ErrorInfo = ''; + + /** + * The From email address for the message. + * + * @var string + */ + public $From = ''; + + /** + * The From name of the message. + * + * @var string + */ + public $FromName = ''; + + /** + * The envelope sender of the message. + * This will usually be turned into a Return-Path header by the receiver, + * and is the address that bounces will be sent to. + * If not empty, will be passed via `-f` to sendmail or as the 'MAIL FROM' value over SMTP. + * + * @var string + */ + public $Sender = ''; + + /** + * The Subject of the message. + * + * @var string + */ + public $Subject = ''; + + /** + * An HTML or plain text message body. + * If HTML then call isHTML(true). + * + * @var string + */ + public $Body = ''; + + /** + * The plain-text message body. + * This body can be read by mail clients that do not have HTML email + * capability such as mutt & Eudora. + * Clients that can read HTML will view the normal Body. + * + * @var string + */ + public $AltBody = ''; + + /** + * An iCal message part body. + * Only supported in simple alt or alt_inline message types + * To generate iCal event structures, use classes like EasyPeasyICS or iCalcreator. + * + * @see http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/ + * @see http://kigkonsult.se/iCalcreator/ + * + * @var string + */ + public $Ical = ''; + + /** + * Value-array of "method" in Contenttype header "text/calendar" + * + * @var string[] + */ + protected static $IcalMethods = [ + self::ICAL_METHOD_REQUEST, + self::ICAL_METHOD_PUBLISH, + self::ICAL_METHOD_REPLY, + self::ICAL_METHOD_ADD, + self::ICAL_METHOD_CANCEL, + self::ICAL_METHOD_REFRESH, + self::ICAL_METHOD_COUNTER, + self::ICAL_METHOD_DECLINECOUNTER, + ]; + + /** + * The complete compiled MIME message body. + * + * @var string + */ + protected $MIMEBody = ''; + + /** + * The complete compiled MIME message headers. + * + * @var string + */ + protected $MIMEHeader = ''; + + /** + * Extra headers that createHeader() doesn't fold in. + * + * @var string + */ + protected $mailHeader = ''; + + /** + * Word-wrap the message body to this number of chars. + * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance. + * + * @see static::STD_LINE_LENGTH + * + * @var int + */ + public $WordWrap = 0; + + /** + * Which method to use to send mail. + * Options: "mail", "sendmail", or "smtp". + * + * @var string + */ + public $Mailer = 'mail'; + + /** + * The path to the sendmail program. + * + * @var string + */ + public $Sendmail = '/usr/sbin/sendmail'; + + /** + * Whether mail() uses a fully sendmail-compatible MTA. + * One which supports sendmail's "-oi -f" options. + * + * @var bool + */ + public $UseSendmailOptions = true; + + /** + * The email address that a reading confirmation should be sent to, also known as read receipt. + * + * @var string + */ + public $ConfirmReadingTo = ''; + + /** + * The hostname to use in the Message-ID header and as default HELO string. + * If empty, PHPMailer attempts to find one with, in order, + * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value + * 'localhost.localdomain'. + * + * @see PHPMailer::$Helo + * + * @var string + */ + public $Hostname = ''; + + /** + * An ID to be used in the Message-ID header. + * If empty, a unique id will be generated. + * You can set your own, but it must be in the format "", + * as defined in RFC5322 section 3.6.4 or it will be ignored. + * + * @see https://tools.ietf.org/html/rfc5322#section-3.6.4 + * + * @var string + */ + public $MessageID = ''; + + /** + * The message Date to be used in the Date header. + * If empty, the current date will be added. + * + * @var string + */ + public $MessageDate = ''; + + /** + * SMTP hosts. + * Either a single hostname or multiple semicolon-delimited hostnames. + * You can also specify a different port + * for each host by using this format: [hostname:port] + * (e.g. "smtp1.example.com:25;smtp2.example.com"). + * You can also specify encryption type, for example: + * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465"). + * Hosts will be tried in order. + * + * @var string + */ + public $Host = 'localhost'; + + /** + * The default SMTP server port. + * + * @var int + */ + public $Port = 25; + + /** + * The SMTP HELO/EHLO name used for the SMTP connection. + * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find + * one with the same method described above for $Hostname. + * + * @see PHPMailer::$Hostname + * + * @var string + */ + public $Helo = ''; + + /** + * What kind of encryption to use on the SMTP connection. + * Options: '', static::ENCRYPTION_STARTTLS, or static::ENCRYPTION_SMTPS. + * + * @var string + */ + public $SMTPSecure = ''; + + /** + * Whether to enable TLS encryption automatically if a server supports it, + * even if `SMTPSecure` is not set to 'tls'. + * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid. + * + * @var bool + */ + public $SMTPAutoTLS = true; + + /** + * Whether to use SMTP authentication. + * Uses the Username and Password properties. + * + * @see PHPMailer::$Username + * @see PHPMailer::$Password + * + * @var bool + */ + public $SMTPAuth = false; + + /** + * Options array passed to stream_context_create when connecting via SMTP. + * + * @var array + */ + public $SMTPOptions = []; + + /** + * SMTP username. + * + * @var string + */ + public $Username = ''; + + /** + * SMTP password. + * + * @var string + */ + public $Password = ''; + + /** + * SMTP authentication type. Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2. + * If not specified, the first one from that list that the server supports will be selected. + * + * @var string + */ + public $AuthType = ''; + + /** + * An implementation of the PHPMailer OAuthTokenProvider interface. + * + * @var OAuthTokenProvider + */ + protected $oauth; + + /** + * The SMTP server timeout in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. + * + * @var int + */ + public $Timeout = 300; + + /** + * Comma separated list of DSN notifications + * 'NEVER' under no circumstances a DSN must be returned to the sender. + * If you use NEVER all other notifications will be ignored. + * 'SUCCESS' will notify you when your mail has arrived at its destination. + * 'FAILURE' will arrive if an error occurred during delivery. + * 'DELAY' will notify you if there is an unusual delay in delivery, but the actual + * delivery's outcome (success or failure) is not yet decided. + * + * @see https://tools.ietf.org/html/rfc3461 See section 4.1 for more information about NOTIFY + */ + public $dsn = ''; + + /** + * SMTP class debug output mode. + * Debug output level. + * Options: + * @see SMTP::DEBUG_OFF: No output + * @see SMTP::DEBUG_CLIENT: Client messages + * @see SMTP::DEBUG_SERVER: Client and server messages + * @see SMTP::DEBUG_CONNECTION: As SERVER plus connection status + * @see SMTP::DEBUG_LOWLEVEL: Noisy, low-level data output, rarely needed + * + * @see SMTP::$do_debug + * + * @var int + */ + public $SMTPDebug = 0; + + /** + * How to handle debug output. + * Options: + * * `echo` Output plain-text as-is, appropriate for CLI + * * `html` Output escaped, line breaks converted to `
`, appropriate for browser output + * * `error_log` Output to error log as configured in php.ini + * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise. + * Alternatively, you can provide a callable expecting two params: a message string and the debug level: + * + * ```php + * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; + * ``` + * + * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug` + * level output is used: + * + * ```php + * $mail->Debugoutput = new myPsr3Logger; + * ``` + * + * @see SMTP::$Debugoutput + * + * @var string|callable|\Psr\Log\LoggerInterface + */ + public $Debugoutput = 'echo'; + + /** + * Whether to keep the SMTP connection open after each message. + * If this is set to true then the connection will remain open after a send, + * and closing the connection will require an explicit call to smtpClose(). + * It's a good idea to use this if you are sending multiple messages as it reduces overhead. + * See the mailing list example for how to use it. + * + * @var bool + */ + public $SMTPKeepAlive = false; + + /** + * Whether to split multiple to addresses into multiple messages + * or send them all in one message. + * Only supported in `mail` and `sendmail` transports, not in SMTP. + * + * @var bool + * + * @deprecated 6.0.0 PHPMailer isn't a mailing list manager! + */ + public $SingleTo = false; + + /** + * Storage for addresses when SingleTo is enabled. + * + * @var array + */ + protected $SingleToArray = []; + + /** + * Whether to generate VERP addresses on send. + * Only applicable when sending via SMTP. + * + * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path + * @see http://www.postfix.org/VERP_README.html Postfix VERP info + * + * @var bool + */ + public $do_verp = false; + + /** + * Whether to allow sending messages with an empty body. + * + * @var bool + */ + public $AllowEmpty = false; + + /** + * DKIM selector. + * + * @var string + */ + public $DKIM_selector = ''; + + /** + * DKIM Identity. + * Usually the email address used as the source of the email. + * + * @var string + */ + public $DKIM_identity = ''; + + /** + * DKIM passphrase. + * Used if your key is encrypted. + * + * @var string + */ + public $DKIM_passphrase = ''; + + /** + * DKIM signing domain name. + * + * @example 'example.com' + * + * @var string + */ + public $DKIM_domain = ''; + + /** + * DKIM Copy header field values for diagnostic use. + * + * @var bool + */ + public $DKIM_copyHeaderFields = true; + + /** + * DKIM Extra signing headers. + * + * @example ['List-Unsubscribe', 'List-Help'] + * + * @var array + */ + public $DKIM_extraHeaders = []; + + /** + * DKIM private key file path. + * + * @var string + */ + public $DKIM_private = ''; + + /** + * DKIM private key string. + * + * If set, takes precedence over `$DKIM_private`. + * + * @var string + */ + public $DKIM_private_string = ''; + + /** + * Callback Action function name. + * + * The function that handles the result of the send email action. + * It is called out by send() for each email sent. + * + * Value can be any php callable: http://www.php.net/is_callable + * + * Parameters: + * bool $result result of the send action + * array $to email addresses of the recipients + * array $cc cc email addresses + * array $bcc bcc email addresses + * string $subject the subject + * string $body the email body + * string $from email address of sender + * string $extra extra information of possible use + * "smtp_transaction_id' => last smtp transaction id + * + * @var string + */ + public $action_function = ''; + + /** + * What to put in the X-Mailer header. + * Options: An empty string for PHPMailer default, whitespace/null for none, or a string to use. + * + * @var string|null + */ + public $XMailer = ''; + + /** + * Which validator to use by default when validating email addresses. + * May be a callable to inject your own validator, but there are several built-in validators. + * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option. + * + * @see PHPMailer::validateAddress() + * + * @var string|callable + */ + public static $validator = 'php'; + + /** + * An instance of the SMTP sender class. + * + * @var SMTP + */ + protected $smtp; + + /** + * The array of 'to' names and addresses. + * + * @var array + */ + protected $to = []; + + /** + * The array of 'cc' names and addresses. + * + * @var array + */ + protected $cc = []; + + /** + * The array of 'bcc' names and addresses. + * + * @var array + */ + protected $bcc = []; + + /** + * The array of reply-to names and addresses. + * + * @var array + */ + protected $ReplyTo = []; + + /** + * An array of all kinds of addresses. + * Includes all of $to, $cc, $bcc. + * + * @see PHPMailer::$to + * @see PHPMailer::$cc + * @see PHPMailer::$bcc + * + * @var array + */ + protected $all_recipients = []; + + /** + * An array of names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $all_recipients + * and one of $to, $cc, or $bcc. + * This array is used only for addresses with IDN. + * + * @see PHPMailer::$to + * @see PHPMailer::$cc + * @see PHPMailer::$bcc + * @see PHPMailer::$all_recipients + * + * @var array + */ + protected $RecipientsQueue = []; + + /** + * An array of reply-to names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $ReplyTo. + * This array is used only for addresses with IDN. + * + * @see PHPMailer::$ReplyTo + * + * @var array + */ + protected $ReplyToQueue = []; + + /** + * The array of attachments. + * + * @var array + */ + protected $attachment = []; + + /** + * The array of custom headers. + * + * @var array + */ + protected $CustomHeader = []; + + /** + * The most recent Message-ID (including angular brackets). + * + * @var string + */ + protected $lastMessageID = ''; + + /** + * The message's MIME type. + * + * @var string + */ + protected $message_type = ''; + + /** + * The array of MIME boundary strings. + * + * @var array + */ + protected $boundary = []; + + /** + * The array of available text strings for the current language. + * + * @var array + */ + protected $language = []; + + /** + * The number of errors encountered. + * + * @var int + */ + protected $error_count = 0; + + /** + * The S/MIME certificate file path. + * + * @var string + */ + protected $sign_cert_file = ''; + + /** + * The S/MIME key file path. + * + * @var string + */ + protected $sign_key_file = ''; + + /** + * The optional S/MIME extra certificates ("CA Chain") file path. + * + * @var string + */ + protected $sign_extracerts_file = ''; + + /** + * The S/MIME password for the key. + * Used only if the key is encrypted. + * + * @var string + */ + protected $sign_key_pass = ''; + + /** + * Whether to throw exceptions for errors. + * + * @var bool + */ + protected $exceptions = false; + + /** + * Unique ID used for message ID and boundaries. + * + * @var string + */ + protected $uniqueid = ''; + + /** + * The PHPMailer Version number. + * + * @var string + */ + const VERSION = '6.8.0'; + + /** + * Error severity: message only, continue processing. + * + * @var int + */ + const STOP_MESSAGE = 0; + + /** + * Error severity: message, likely ok to continue processing. + * + * @var int + */ + const STOP_CONTINUE = 1; + + /** + * Error severity: message, plus full stop, critical error reached. + * + * @var int + */ + const STOP_CRITICAL = 2; + + /** + * The SMTP standard CRLF line break. + * If you want to change line break format, change static::$LE, not this. + */ + const CRLF = "\r\n"; + + /** + * "Folding White Space" a white space string used for line folding. + */ + const FWS = ' '; + + /** + * SMTP RFC standard line ending; Carriage Return, Line Feed. + * + * @var string + */ + protected static $LE = self::CRLF; + + /** + * The maximum line length supported by mail(). + * + * Background: mail() will sometimes corrupt messages + * with headers headers longer than 65 chars, see #818. + * + * @var int + */ + const MAIL_MAX_LINE_LENGTH = 63; + + /** + * The maximum line length allowed by RFC 2822 section 2.1.1. + * + * @var int + */ + const MAX_LINE_LENGTH = 998; + + /** + * The lower maximum line length allowed by RFC 2822 section 2.1.1. + * This length does NOT include the line break + * 76 means that lines will be 77 or 78 chars depending on whether + * the line break format is LF or CRLF; both are valid. + * + * @var int + */ + const STD_LINE_LENGTH = 76; + + /** + * Constructor. + * + * @param bool $exceptions Should we throw external exceptions? + */ + public function __construct($exceptions = null) + { + if (null !== $exceptions) { + $this->exceptions = (bool) $exceptions; + } + //Pick an appropriate debug output format automatically + $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html'); + } + + /** + * Destructor. + */ + public function __destruct() + { + //Close any open SMTP connection nicely + $this->smtpClose(); + } + + /** + * Call mail() in a safe_mode-aware fashion. + * Also, unless sendmail_path points to sendmail (or something that + * claims to be sendmail), don't pass params (not a perfect fix, + * but it will do). + * + * @param string $to To + * @param string $subject Subject + * @param string $body Message Body + * @param string $header Additional Header(s) + * @param string|null $params Params + * + * @return bool + */ + private function mailPassthru($to, $subject, $body, $header, $params) + { + //Check overloading of mail function to avoid double-encoding + if ((int)ini_get('mbstring.func_overload') & 1) { + $subject = $this->secureHeader($subject); + } else { + $subject = $this->encodeHeader($this->secureHeader($subject)); + } + //Calling mail() with null params breaks + $this->edebug('Sending with mail()'); + $this->edebug('Sendmail path: ' . ini_get('sendmail_path')); + $this->edebug("Envelope sender: {$this->Sender}"); + $this->edebug("To: {$to}"); + $this->edebug("Subject: {$subject}"); + $this->edebug("Headers: {$header}"); + if (!$this->UseSendmailOptions || null === $params) { + $result = @mail($to, $subject, $body, $header); + } else { + $this->edebug("Additional params: {$params}"); + $result = @mail($to, $subject, $body, $header, $params); + } + $this->edebug('Result: ' . ($result ? 'true' : 'false')); + return $result; + } + + /** + * Output debugging info via a user-defined method. + * Only generates output if debug output is enabled. + * + * @see PHPMailer::$Debugoutput + * @see PHPMailer::$SMTPDebug + * + * @param string $str + */ + protected function edebug($str) + { + if ($this->SMTPDebug <= 0) { + return; + } + //Is this a PSR-3 logger? + if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) { + $this->Debugoutput->debug($str); + + return; + } + //Avoid clash with built-in function names + if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) { + call_user_func($this->Debugoutput, $str, $this->SMTPDebug); + + return; + } + switch ($this->Debugoutput) { + case 'error_log': + //Don't output, just log + /** @noinspection ForgottenDebugOutputInspection */ + error_log($str); + break; + case 'html': + //Cleans up output a bit for a better looking, HTML-safe output + echo htmlentities( + preg_replace('/[\r\n]+/', '', $str), + ENT_QUOTES, + 'UTF-8' + ), "
\n"; + break; + case 'echo': + default: + //Normalize line breaks + $str = preg_replace('/\r\n|\r/m', "\n", $str); + echo gmdate('Y-m-d H:i:s'), + "\t", + //Trim trailing space + trim( + //Indent for readability, except for trailing break + str_replace( + "\n", + "\n \t ", + trim($str) + ) + ), + "\n"; + } + } + + /** + * Sets message type to HTML or plain. + * + * @param bool $isHtml True for HTML mode + */ + public function isHTML($isHtml = true) + { + if ($isHtml) { + $this->ContentType = static::CONTENT_TYPE_TEXT_HTML; + } else { + $this->ContentType = static::CONTENT_TYPE_PLAINTEXT; + } + } + + /** + * Send messages using SMTP. + */ + public function isSMTP() + { + $this->Mailer = 'smtp'; + } + + /** + * Send messages using PHP's mail() function. + */ + public function isMail() + { + $this->Mailer = 'mail'; + } + + /** + * Send messages using $Sendmail. + */ + public function isSendmail() + { + $ini_sendmail_path = ini_get('sendmail_path'); + + if (false === stripos($ini_sendmail_path, 'sendmail')) { + $this->Sendmail = '/usr/sbin/sendmail'; + } else { + $this->Sendmail = $ini_sendmail_path; + } + $this->Mailer = 'sendmail'; + } + + /** + * Send messages using qmail. + */ + public function isQmail() + { + $ini_sendmail_path = ini_get('sendmail_path'); + + if (false === stripos($ini_sendmail_path, 'qmail')) { + $this->Sendmail = '/var/qmail/bin/qmail-inject'; + } else { + $this->Sendmail = $ini_sendmail_path; + } + $this->Mailer = 'qmail'; + } + + /** + * Add a "To" address. + * + * @param string $address The email address to send to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addAddress($address, $name = '') + { + return $this->addOrEnqueueAnAddress('to', $address, $name); + } + + /** + * Add a "CC" address. + * + * @param string $address The email address to send to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addCC($address, $name = '') + { + return $this->addOrEnqueueAnAddress('cc', $address, $name); + } + + /** + * Add a "BCC" address. + * + * @param string $address The email address to send to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addBCC($address, $name = '') + { + return $this->addOrEnqueueAnAddress('bcc', $address, $name); + } + + /** + * Add a "Reply-To" address. + * + * @param string $address The email address to reply to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addReplyTo($address, $name = '') + { + return $this->addOrEnqueueAnAddress('Reply-To', $address, $name); + } + + /** + * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer + * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still + * be modified after calling this function), addition of such addresses is delayed until send(). + * Addresses that have been added already return false, but do not throw exceptions. + * + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address + * @param string $name An optional username associated with the address + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + protected function addOrEnqueueAnAddress($kind, $address, $name) + { + $pos = false; + if ($address !== null) { + $address = trim($address); + $pos = strrpos($address, '@'); + } + if (false === $pos) { + //At-sign is missing. + $error_message = sprintf( + '%s (%s): %s', + $this->lang('invalid_address'), + $kind, + $address + ); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + if ($name !== null && is_string($name)) { + $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + } else { + $name = ''; + } + $params = [$kind, $address, $name]; + //Enqueue addresses with IDN until we know the PHPMailer::$CharSet. + //Domain is assumed to be whatever is after the last @ symbol in the address + if (static::idnSupported() && $this->has8bitChars(substr($address, ++$pos))) { + if ('Reply-To' !== $kind) { + if (!array_key_exists($address, $this->RecipientsQueue)) { + $this->RecipientsQueue[$address] = $params; + + return true; + } + } elseif (!array_key_exists($address, $this->ReplyToQueue)) { + $this->ReplyToQueue[$address] = $params; + + return true; + } + + return false; + } + + //Immediately add standard addresses without IDN. + return call_user_func_array([$this, 'addAnAddress'], $params); + } + + /** + * Set the boundaries to use for delimiting MIME parts. + * If you override this, ensure you set all 3 boundaries to unique values. + * The default boundaries include a "=_" sequence which cannot occur in quoted-printable bodies, + * as suggested by https://www.rfc-editor.org/rfc/rfc2045#section-6.7 + * + * @return void + */ + public function setBoundaries() + { + $this->uniqueid = $this->generateId(); + $this->boundary[1] = 'b1=_' . $this->uniqueid; + $this->boundary[2] = 'b2=_' . $this->uniqueid; + $this->boundary[3] = 'b3=_' . $this->uniqueid; + } + + /** + * Add an address to one of the recipient arrays or to the ReplyTo array. + * Addresses that have been added already return false, but do not throw exceptions. + * + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address to send, resp. to reply to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + protected function addAnAddress($kind, $address, $name = '') + { + if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) { + $error_message = sprintf( + '%s: %s', + $this->lang('Invalid recipient kind'), + $kind + ); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + if (!static::validateAddress($address)) { + $error_message = sprintf( + '%s (%s): %s', + $this->lang('invalid_address'), + $kind, + $address + ); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + if ('Reply-To' !== $kind) { + if (!array_key_exists(strtolower($address), $this->all_recipients)) { + $this->{$kind}[] = [$address, $name]; + $this->all_recipients[strtolower($address)] = true; + + return true; + } + } elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) { + $this->ReplyTo[strtolower($address)] = [$address, $name]; + + return true; + } + + return false; + } + + /** + * Parse and validate a string containing one or more RFC822-style comma-separated email addresses + * of the form "display name
" into an array of name/address pairs. + * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available. + * Note that quotes in the name part are removed. + * + * @see http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation + * + * @param string $addrstr The address list string + * @param bool $useimap Whether to use the IMAP extension to parse the list + * @param string $charset The charset to use when decoding the address list string. + * + * @return array + */ + public static function parseAddresses($addrstr, $useimap = true, $charset = self::CHARSET_ISO88591) + { + $addresses = []; + if ($useimap && function_exists('imap_rfc822_parse_adrlist')) { + //Use this built-in parser if it's available + $list = imap_rfc822_parse_adrlist($addrstr, ''); + // Clear any potential IMAP errors to get rid of notices being thrown at end of script. + imap_errors(); + foreach ($list as $address) { + if ( + '.SYNTAX-ERROR.' !== $address->host && + static::validateAddress($address->mailbox . '@' . $address->host) + ) { + //Decode the name part if it's present and encoded + if ( + property_exists($address, 'personal') && + //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled + defined('MB_CASE_UPPER') && + preg_match('/^=\?.*\?=$/s', $address->personal) + ) { + $origCharset = mb_internal_encoding(); + mb_internal_encoding($charset); + //Undo any RFC2047-encoded spaces-as-underscores + $address->personal = str_replace('_', '=20', $address->personal); + //Decode the name + $address->personal = mb_decode_mimeheader($address->personal); + mb_internal_encoding($origCharset); + } + + $addresses[] = [ + 'name' => (property_exists($address, 'personal') ? $address->personal : ''), + 'address' => $address->mailbox . '@' . $address->host, + ]; + } + } + } else { + //Use this simpler parser + $list = explode(',', $addrstr); + foreach ($list as $address) { + $address = trim($address); + //Is there a separate name part? + if (strpos($address, '<') === false) { + //No separate name, just use the whole thing + if (static::validateAddress($address)) { + $addresses[] = [ + 'name' => '', + 'address' => $address, + ]; + } + } else { + list($name, $email) = explode('<', $address); + $email = trim(str_replace('>', '', $email)); + $name = trim($name); + if (static::validateAddress($email)) { + //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled + //If this name is encoded, decode it + if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) { + $origCharset = mb_internal_encoding(); + mb_internal_encoding($charset); + //Undo any RFC2047-encoded spaces-as-underscores + $name = str_replace('_', '=20', $name); + //Decode the name + $name = mb_decode_mimeheader($name); + mb_internal_encoding($origCharset); + } + $addresses[] = [ + //Remove any surrounding quotes and spaces from the name + 'name' => trim($name, '\'" '), + 'address' => $email, + ]; + } + } + } + } + + return $addresses; + } + + /** + * Set the From and FromName properties. + * + * @param string $address + * @param string $name + * @param bool $auto Whether to also set the Sender address, defaults to true + * + * @throws Exception + * + * @return bool + */ + public function setFrom($address, $name = '', $auto = true) + { + $address = trim((string)$address); + $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + //Don't validate now addresses with IDN. Will be done in send(). + $pos = strrpos($address, '@'); + if ( + (false === $pos) + || ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnSupported()) + && !static::validateAddress($address)) + ) { + $error_message = sprintf( + '%s (From): %s', + $this->lang('invalid_address'), + $address + ); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + $this->From = $address; + $this->FromName = $name; + if ($auto && empty($this->Sender)) { + $this->Sender = $address; + } + + return true; + } + + /** + * Return the Message-ID header of the last email. + * Technically this is the value from the last time the headers were created, + * but it's also the message ID of the last sent message except in + * pathological cases. + * + * @return string + */ + public function getLastMessageID() + { + return $this->lastMessageID; + } + + /** + * Check that a string looks like an email address. + * Validation patterns supported: + * * `auto` Pick best pattern automatically; + * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0; + * * `pcre` Use old PCRE implementation; + * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL; + * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements. + * * `noregex` Don't use a regex: super fast, really dumb. + * Alternatively you may pass in a callable to inject your own validator, for example: + * + * ```php + * PHPMailer::validateAddress('user@example.com', function($address) { + * return (strpos($address, '@') !== false); + * }); + * ``` + * + * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator. + * + * @param string $address The email address to check + * @param string|callable $patternselect Which pattern to use + * + * @return bool + */ + public static function validateAddress($address, $patternselect = null) + { + if (null === $patternselect) { + $patternselect = static::$validator; + } + //Don't allow strings as callables, see SECURITY.md and CVE-2021-3603 + if (is_callable($patternselect) && !is_string($patternselect)) { + return call_user_func($patternselect, $address); + } + //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321 + if (strpos($address, "\n") !== false || strpos($address, "\r") !== false) { + return false; + } + switch ($patternselect) { + case 'pcre': //Kept for BC + case 'pcre8': + /* + * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL + * is based. + * In addition to the addresses allowed by filter_var, also permits: + * * dotless domains: `a@b` + * * comments: `1234 @ local(blah) .machine .example` + * * quoted elements: `'"test blah"@example.org'` + * * numeric TLDs: `a@b.123` + * * unbracketed IPv4 literals: `a@192.168.0.1` + * * IPv6 literals: 'first.last@[IPv6:a1::]' + * Not all of these will necessarily work for sending! + * + * @see http://squiloople.com/2009/12/20/email-address-validation/ + * @copyright 2009-2010 Michael Rushton + * Feel free to use and redistribute this code. But please keep this copyright notice. + */ + return (bool) preg_match( + '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' . + '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' . + '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' . + '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' . + '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' . + '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' . + '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' . + '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' . + '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD', + $address + ); + case 'html5': + /* + * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements. + * + * @see https://html.spec.whatwg.org/#e-mail-state-(type=email) + */ + return (bool) preg_match( + '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' . + '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD', + $address + ); + case 'php': + default: + return filter_var($address, FILTER_VALIDATE_EMAIL) !== false; + } + } + + /** + * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the + * `intl` and `mbstring` PHP extensions. + * + * @return bool `true` if required functions for IDN support are present + */ + public static function idnSupported() + { + return function_exists('idn_to_ascii') && function_exists('mb_convert_encoding'); + } + + /** + * Converts IDN in given email address to its ASCII form, also known as punycode, if possible. + * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet. + * This function silently returns unmodified address if: + * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form) + * - Conversion to punycode is impossible (e.g. required PHP functions are not available) + * or fails for any reason (e.g. domain contains characters not allowed in an IDN). + * + * @see PHPMailer::$CharSet + * + * @param string $address The email address to convert + * + * @return string The encoded address in ASCII form + */ + public function punyencodeAddress($address) + { + //Verify we have required functions, CharSet, and at-sign. + $pos = strrpos($address, '@'); + if ( + !empty($this->CharSet) && + false !== $pos && + static::idnSupported() + ) { + $domain = substr($address, ++$pos); + //Verify CharSet string is a valid one, and domain properly encoded in this CharSet. + if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) { + //Convert the domain from whatever charset it's in to UTF-8 + $domain = mb_convert_encoding($domain, self::CHARSET_UTF8, $this->CharSet); + //Ignore IDE complaints about this line - method signature changed in PHP 5.4 + $errorcode = 0; + if (defined('INTL_IDNA_VARIANT_UTS46')) { + //Use the current punycode standard (appeared in PHP 7.2) + $punycode = idn_to_ascii( + $domain, + \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI | + \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII, + \INTL_IDNA_VARIANT_UTS46 + ); + } elseif (defined('INTL_IDNA_VARIANT_2003')) { + //Fall back to this old, deprecated/removed encoding + $punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VARIANT_2003); + } else { + //Fall back to a default we don't know about + $punycode = idn_to_ascii($domain, $errorcode); + } + if (false !== $punycode) { + return substr($address, 0, $pos) . $punycode; + } + } + } + + return $address; + } + + /** + * Create a message and send it. + * Uses the sending method specified by $Mailer. + * + * @throws Exception + * + * @return bool false on error - See the ErrorInfo property for details of the error + */ + public function send() + { + try { + if (!$this->preSend()) { + return false; + } + + return $this->postSend(); + } catch (Exception $exc) { + $this->mailHeader = ''; + $this->setError($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + + return false; + } + } + + /** + * Prepare a message for sending. + * + * @throws Exception + * + * @return bool + */ + public function preSend() + { + if ( + 'smtp' === $this->Mailer + || ('mail' === $this->Mailer && (\PHP_VERSION_ID >= 80000 || stripos(PHP_OS, 'WIN') === 0)) + ) { + //SMTP mandates RFC-compliant line endings + //and it's also used with mail() on Windows + static::setLE(self::CRLF); + } else { + //Maintain backward compatibility with legacy Linux command line mailers + static::setLE(PHP_EOL); + } + //Check for buggy PHP versions that add a header with an incorrect line break + if ( + 'mail' === $this->Mailer + && ((\PHP_VERSION_ID >= 70000 && \PHP_VERSION_ID < 70017) + || (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70103)) + && ini_get('mail.add_x_header') === '1' + && stripos(PHP_OS, 'WIN') === 0 + ) { + trigger_error($this->lang('buggy_php'), E_USER_WARNING); + } + + try { + $this->error_count = 0; //Reset errors + $this->mailHeader = ''; + + //Dequeue recipient and Reply-To addresses with IDN + foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) { + $params[1] = $this->punyencodeAddress($params[1]); + call_user_func_array([$this, 'addAnAddress'], $params); + } + if (count($this->to) + count($this->cc) + count($this->bcc) < 1) { + throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL); + } + + //Validate From, Sender, and ConfirmReadingTo addresses + foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) { + $this->{$address_kind} = trim($this->{$address_kind}); + if (empty($this->{$address_kind})) { + continue; + } + $this->{$address_kind} = $this->punyencodeAddress($this->{$address_kind}); + if (!static::validateAddress($this->{$address_kind})) { + $error_message = sprintf( + '%s (%s): %s', + $this->lang('invalid_address'), + $address_kind, + $this->{$address_kind} + ); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + } + + //Set whether the message is multipart/alternative + if ($this->alternativeExists()) { + $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE; + } + + $this->setMessageType(); + //Refuse to send an empty message unless we are specifically allowing it + if (!$this->AllowEmpty && empty($this->Body)) { + throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); + } + + //Trim subject consistently + $this->Subject = trim($this->Subject); + //Create body before headers in case body makes changes to headers (e.g. altering transfer encoding) + $this->MIMEHeader = ''; + $this->MIMEBody = $this->createBody(); + //createBody may have added some headers, so retain them + $tempheaders = $this->MIMEHeader; + $this->MIMEHeader = $this->createHeader(); + $this->MIMEHeader .= $tempheaders; + + //To capture the complete message when using mail(), create + //an extra header list which createHeader() doesn't fold in + if ('mail' === $this->Mailer) { + if (count($this->to) > 0) { + $this->mailHeader .= $this->addrAppend('To', $this->to); + } else { + $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;'); + } + $this->mailHeader .= $this->headerLine( + 'Subject', + $this->encodeHeader($this->secureHeader($this->Subject)) + ); + } + + //Sign with DKIM if enabled + if ( + !empty($this->DKIM_domain) + && !empty($this->DKIM_selector) + && (!empty($this->DKIM_private_string) + || (!empty($this->DKIM_private) + && static::isPermittedPath($this->DKIM_private) + && file_exists($this->DKIM_private) + ) + ) + ) { + $header_dkim = $this->DKIM_Add( + $this->MIMEHeader . $this->mailHeader, + $this->encodeHeader($this->secureHeader($this->Subject)), + $this->MIMEBody + ); + $this->MIMEHeader = static::stripTrailingWSP($this->MIMEHeader) . static::$LE . + static::normalizeBreaks($header_dkim) . static::$LE; + } + + return true; + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + + return false; + } + } + + /** + * Actually send a message via the selected mechanism. + * + * @throws Exception + * + * @return bool + */ + public function postSend() + { + try { + //Choose the mailer and send through it + switch ($this->Mailer) { + case 'sendmail': + case 'qmail': + return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody); + case 'smtp': + return $this->smtpSend($this->MIMEHeader, $this->MIMEBody); + case 'mail': + return $this->mailSend($this->MIMEHeader, $this->MIMEBody); + default: + $sendMethod = $this->Mailer . 'Send'; + if (method_exists($this, $sendMethod)) { + return $this->{$sendMethod}($this->MIMEHeader, $this->MIMEBody); + } + + return $this->mailSend($this->MIMEHeader, $this->MIMEBody); + } + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + $this->edebug($exc->getMessage()); + if ($this->Mailer === 'smtp' && $this->SMTPKeepAlive == true && $this->smtp->connected()) { + $this->smtp->reset(); + } + if ($this->exceptions) { + throw $exc; + } + } + + return false; + } + + /** + * Send mail using the $Sendmail program. + * + * @see PHPMailer::$Sendmail + * + * @param string $header The message headers + * @param string $body The message body + * + * @throws Exception + * + * @return bool + */ + protected function sendmailSend($header, $body) + { + if ($this->Mailer === 'qmail') { + $this->edebug('Sending with qmail'); + } else { + $this->edebug('Sending with sendmail'); + } + $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; + //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver + //A space after `-f` is optional, but there is a long history of its presence + //causing problems, so we don't use one + //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html + //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html + //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html + //Example problem: https://www.drupal.org/node/1057954 + + //PHP 5.6 workaround + $sendmail_from_value = ini_get('sendmail_from'); + if (empty($this->Sender) && !empty($sendmail_from_value)) { + //PHP config has a sender address we can use + $this->Sender = ini_get('sendmail_from'); + } + //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. + if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) { + if ($this->Mailer === 'qmail') { + $sendmailFmt = '%s -f%s'; + } else { + $sendmailFmt = '%s -oi -f%s -t'; + } + } else { + //allow sendmail to choose a default envelope sender. It may + //seem preferable to force it to use the From header as with + //SMTP, but that introduces new problems (see + //), and + //it has historically worked this way. + $sendmailFmt = '%s -oi -t'; + } + + $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender); + $this->edebug('Sendmail path: ' . $this->Sendmail); + $this->edebug('Sendmail command: ' . $sendmail); + $this->edebug('Envelope sender: ' . $this->Sender); + $this->edebug("Headers: {$header}"); + + if ($this->SingleTo) { + foreach ($this->SingleToArray as $toAddr) { + $mail = @popen($sendmail, 'w'); + if (!$mail) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + $this->edebug("To: {$toAddr}"); + fwrite($mail, 'To: ' . $toAddr . "\n"); + fwrite($mail, $header); + fwrite($mail, $body); + $result = pclose($mail); + $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet); + $this->doCallback( + ($result === 0), + [[$addrinfo['address'], $addrinfo['name']]], + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); + if (0 !== $result) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + } + } else { + $mail = @popen($sendmail, 'w'); + if (!$mail) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + fwrite($mail, $header); + fwrite($mail, $body); + $result = pclose($mail); + $this->doCallback( + ($result === 0), + $this->to, + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); + if (0 !== $result) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + } + + return true; + } + + /** + * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters. + * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows. + * + * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report + * + * @param string $string The string to be validated + * + * @return bool + */ + protected static function isShellSafe($string) + { + //It's not possible to use shell commands safely (which includes the mail() function) without escapeshellarg, + //but some hosting providers disable it, creating a security problem that we don't want to have to deal with, + //so we don't. + if (!function_exists('escapeshellarg') || !function_exists('escapeshellcmd')) { + return false; + } + + if ( + escapeshellcmd($string) !== $string + || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""]) + ) { + return false; + } + + $length = strlen($string); + + for ($i = 0; $i < $length; ++$i) { + $c = $string[$i]; + + //All other characters have a special meaning in at least one common shell, including = and +. + //Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here. + //Note that this does permit non-Latin alphanumeric characters based on the current locale. + if (!ctype_alnum($c) && strpos('@_-.', $c) === false) { + return false; + } + } + + return true; + } + + /** + * Check whether a file path is of a permitted type. + * Used to reject URLs and phar files from functions that access local file paths, + * such as addAttachment. + * + * @param string $path A relative or absolute path to a file + * + * @return bool + */ + protected static function isPermittedPath($path) + { + //Matches scheme definition from https://tools.ietf.org/html/rfc3986#section-3.1 + return !preg_match('#^[a-z][a-z\d+.-]*://#i', $path); + } + + /** + * Check whether a file path is safe, accessible, and readable. + * + * @param string $path A relative or absolute path to a file + * + * @return bool + */ + protected static function fileIsAccessible($path) + { + if (!static::isPermittedPath($path)) { + return false; + } + $readable = is_file($path); + //If not a UNC path (expected to start with \\), check read permission, see #2069 + if (strpos($path, '\\\\') !== 0) { + $readable = $readable && is_readable($path); + } + return $readable; + } + + /** + * Send mail using the PHP mail() function. + * + * @see http://www.php.net/manual/en/book.mail.php + * + * @param string $header The message headers + * @param string $body The message body + * + * @throws Exception + * + * @return bool + */ + protected function mailSend($header, $body) + { + $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; + + $toArr = []; + foreach ($this->to as $toaddr) { + $toArr[] = $this->addrFormat($toaddr); + } + $to = trim(implode(', ', $toArr)); + + //If there are no To-addresses (e.g. when sending only to BCC-addresses) + //the following should be added to get a correct DKIM-signature. + //Compare with $this->preSend() + if ($to === '') { + $to = 'undisclosed-recipients:;'; + } + + $params = null; + //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver + //A space after `-f` is optional, but there is a long history of its presence + //causing problems, so we don't use one + //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html + //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html + //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html + //Example problem: https://www.drupal.org/node/1057954 + //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. + + //PHP 5.6 workaround + $sendmail_from_value = ini_get('sendmail_from'); + if (empty($this->Sender) && !empty($sendmail_from_value)) { + //PHP config has a sender address we can use + $this->Sender = ini_get('sendmail_from'); + } + if (!empty($this->Sender) && static::validateAddress($this->Sender)) { + if (self::isShellSafe($this->Sender)) { + $params = sprintf('-f%s', $this->Sender); + } + $old_from = ini_get('sendmail_from'); + ini_set('sendmail_from', $this->Sender); + } + $result = false; + if ($this->SingleTo && count($toArr) > 1) { + foreach ($toArr as $toAddr) { + $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params); + $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet); + $this->doCallback( + $result, + [[$addrinfo['address'], $addrinfo['name']]], + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + } + } else { + $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params); + $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []); + } + if (isset($old_from)) { + ini_set('sendmail_from', $old_from); + } + if (!$result) { + throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL); + } + + return true; + } + + /** + * Get an instance to use for SMTP operations. + * Override this function to load your own SMTP implementation, + * or set one with setSMTPInstance. + * + * @return SMTP + */ + public function getSMTPInstance() + { + if (!is_object($this->smtp)) { + $this->smtp = new SMTP(); + } + + return $this->smtp; + } + + /** + * Provide an instance to use for SMTP operations. + * + * @return SMTP + */ + public function setSMTPInstance(SMTP $smtp) + { + $this->smtp = $smtp; + + return $this->smtp; + } + + /** + * Send mail via SMTP. + * Returns false if there is a bad MAIL FROM, RCPT, or DATA input. + * + * @see PHPMailer::setSMTPInstance() to use a different class. + * + * @uses \PHPMailer\PHPMailer\SMTP + * + * @param string $header The message headers + * @param string $body The message body + * + * @throws Exception + * + * @return bool + */ + protected function smtpSend($header, $body) + { + $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; + $bad_rcpt = []; + if (!$this->smtpConnect($this->SMTPOptions)) { + throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL); + } + //Sender already validated in preSend() + if ('' === $this->Sender) { + $smtp_from = $this->From; + } else { + $smtp_from = $this->Sender; + } + if (!$this->smtp->mail($smtp_from)) { + $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); + throw new Exception($this->ErrorInfo, self::STOP_CRITICAL); + } + + $callbacks = []; + //Attempt to send to all recipients + foreach ([$this->to, $this->cc, $this->bcc] as $togroup) { + foreach ($togroup as $to) { + if (!$this->smtp->recipient($to[0], $this->dsn)) { + $error = $this->smtp->getError(); + $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']]; + $isSent = false; + } else { + $isSent = true; + } + + $callbacks[] = ['issent' => $isSent, 'to' => $to[0], 'name' => $to[1]]; + } + } + + //Only send the DATA command if we have viable recipients + if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) { + throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL); + } + + $smtp_transaction_id = $this->smtp->getLastTransactionID(); + + if ($this->SMTPKeepAlive) { + $this->smtp->reset(); + } else { + $this->smtp->quit(); + $this->smtp->close(); + } + + foreach ($callbacks as $cb) { + $this->doCallback( + $cb['issent'], + [[$cb['to'], $cb['name']]], + [], + [], + $this->Subject, + $body, + $this->From, + ['smtp_transaction_id' => $smtp_transaction_id] + ); + } + + //Create error message for any bad addresses + if (count($bad_rcpt) > 0) { + $errstr = ''; + foreach ($bad_rcpt as $bad) { + $errstr .= $bad['to'] . ': ' . $bad['error']; + } + throw new Exception($this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE); + } + + return true; + } + + /** + * Initiate a connection to an SMTP server. + * Returns false if the operation failed. + * + * @param array $options An array of options compatible with stream_context_create() + * + * @throws Exception + * + * @uses \PHPMailer\PHPMailer\SMTP + * + * @return bool + */ + public function smtpConnect($options = null) + { + if (null === $this->smtp) { + $this->smtp = $this->getSMTPInstance(); + } + + //If no options are provided, use whatever is set in the instance + if (null === $options) { + $options = $this->SMTPOptions; + } + + //Already connected? + if ($this->smtp->connected()) { + return true; + } + + $this->smtp->setTimeout($this->Timeout); + $this->smtp->setDebugLevel($this->SMTPDebug); + $this->smtp->setDebugOutput($this->Debugoutput); + $this->smtp->setVerp($this->do_verp); + if ($this->Host === null) { + $this->Host = 'localhost'; + } + $hosts = explode(';', $this->Host); + $lastexception = null; + + foreach ($hosts as $hostentry) { + $hostinfo = []; + if ( + !preg_match( + '/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/', + trim($hostentry), + $hostinfo + ) + ) { + $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry)); + //Not a valid host entry + continue; + } + //$hostinfo[1]: optional ssl or tls prefix + //$hostinfo[2]: the hostname + //$hostinfo[3]: optional port number + //The host string prefix can temporarily override the current setting for SMTPSecure + //If it's not specified, the default value is used + + //Check the host name is a valid name or IP address before trying to use it + if (!static::isValidHost($hostinfo[2])) { + $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]); + continue; + } + $prefix = ''; + $secure = $this->SMTPSecure; + $tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure); + if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYPTION_SMTPS === $this->SMTPSecure)) { + $prefix = 'ssl://'; + $tls = false; //Can't have SSL and TLS at the same time + $secure = static::ENCRYPTION_SMTPS; + } elseif ('tls' === $hostinfo[1]) { + $tls = true; + //TLS doesn't use a prefix + $secure = static::ENCRYPTION_STARTTLS; + } + //Do we need the OpenSSL extension? + $sslext = defined('OPENSSL_ALGO_SHA256'); + if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) { + //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled + if (!$sslext) { + throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL); + } + } + $host = $hostinfo[2]; + $port = $this->Port; + if ( + array_key_exists(3, $hostinfo) && + is_numeric($hostinfo[3]) && + $hostinfo[3] > 0 && + $hostinfo[3] < 65536 + ) { + $port = (int) $hostinfo[3]; + } + if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) { + try { + if ($this->Helo) { + $hello = $this->Helo; + } else { + $hello = $this->serverHostname(); + } + $this->smtp->hello($hello); + //Automatically enable TLS encryption if: + //* it's not disabled + //* we have openssl extension + //* we are not already using SSL + //* the server offers STARTTLS + if ($this->SMTPAutoTLS && $sslext && 'ssl' !== $secure && $this->smtp->getServerExt('STARTTLS')) { + $tls = true; + } + if ($tls) { + if (!$this->smtp->startTLS()) { + $message = $this->getSmtpErrorMessage('connect_host'); + throw new Exception($message); + } + //We must resend EHLO after TLS negotiation + $this->smtp->hello($hello); + } + if ( + $this->SMTPAuth && !$this->smtp->authenticate( + $this->Username, + $this->Password, + $this->AuthType, + $this->oauth + ) + ) { + throw new Exception($this->lang('authenticate')); + } + + return true; + } catch (Exception $exc) { + $lastexception = $exc; + $this->edebug($exc->getMessage()); + //We must have connected, but then failed TLS or Auth, so close connection nicely + $this->smtp->quit(); + } + } + } + //If we get here, all connection attempts have failed, so close connection hard + $this->smtp->close(); + //As we've caught all exceptions, just report whatever the last one was + if ($this->exceptions && null !== $lastexception) { + throw $lastexception; + } + if ($this->exceptions) { + // no exception was thrown, likely $this->smtp->connect() failed + $message = $this->getSmtpErrorMessage('connect_host'); + throw new Exception($message); + } + + return false; + } + + /** + * Close the active SMTP session if one exists. + */ + public function smtpClose() + { + if ((null !== $this->smtp) && $this->smtp->connected()) { + $this->smtp->quit(); + $this->smtp->close(); + } + } + + /** + * Set the language for error messages. + * The default language is English. + * + * @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr") + * Optionally, the language code can be enhanced with a 4-character + * script annotation and/or a 2-character country annotation. + * @param string $lang_path Path to the language file directory, with trailing separator (slash) + * Do not set this from user input! + * + * @return bool Returns true if the requested language was loaded, false otherwise. + */ + public function setLanguage($langcode = 'en', $lang_path = '') + { + //Backwards compatibility for renamed language codes + $renamed_langcodes = [ + 'br' => 'pt_br', + 'cz' => 'cs', + 'dk' => 'da', + 'no' => 'nb', + 'se' => 'sv', + 'rs' => 'sr', + 'tg' => 'tl', + 'am' => 'hy', + ]; + + if (array_key_exists($langcode, $renamed_langcodes)) { + $langcode = $renamed_langcodes[$langcode]; + } + + //Define full set of translatable strings in English + $PHPMAILER_LANG = [ + 'authenticate' => 'SMTP Error: Could not authenticate.', + 'buggy_php' => 'Your version of PHP is affected by a bug that may result in corrupted messages.' . + ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' . + ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.', + 'connect_host' => 'SMTP Error: Could not connect to SMTP host.', + 'data_not_accepted' => 'SMTP Error: data not accepted.', + 'empty_message' => 'Message body empty', + 'encoding' => 'Unknown encoding: ', + 'execute' => 'Could not execute: ', + 'extension_missing' => 'Extension missing: ', + 'file_access' => 'Could not access file: ', + 'file_open' => 'File Error: Could not open file: ', + 'from_failed' => 'The following From address failed: ', + 'instantiate' => 'Could not instantiate mail function.', + 'invalid_address' => 'Invalid address: ', + 'invalid_header' => 'Invalid header name or value', + 'invalid_hostentry' => 'Invalid hostentry: ', + 'invalid_host' => 'Invalid host: ', + 'mailer_not_supported' => ' mailer is not supported.', + 'provide_address' => 'You must provide at least one recipient email address.', + 'recipients_failed' => 'SMTP Error: The following recipients failed: ', + 'signing' => 'Signing Error: ', + 'smtp_code' => 'SMTP code: ', + 'smtp_code_ex' => 'Additional SMTP info: ', + 'smtp_connect_failed' => 'SMTP connect() failed.', + 'smtp_detail' => 'Detail: ', + 'smtp_error' => 'SMTP server error: ', + 'variable_set' => 'Cannot set or reset variable: ', + ]; + if (empty($lang_path)) { + //Calculate an absolute path so it can work if CWD is not here + $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR; + } + + //Validate $langcode + $foundlang = true; + $langcode = strtolower($langcode); + if ( + !preg_match('/^(?P[a-z]{2})(?P